Threads

Les threads1), comme les processus, sont un mécanisme permettant à un programme de faire plus d’une chose à la fois. Comme les processus, les threads semblent s’exécuter en parallèle; le noyau Linux les ordonnance de façon asynchrone, interrompant chaque thread de temps en temps pour donner aux autres une chance de s’exécuter.

Conceptuellement, un thread existe au sein d’un processus. Les threads sont une unité d’exécution plus fine que les processus. Lorsque vous invoquez un programme, Linux crée un nouveau processus et, dans ce processus, crée un thread, qui exécute le processus de façon séquentielle. Ce thread peut en créer d’autres; tous ces threads exécutent alors le même programme au sein du même processus, mais chaque thread peut exécuter une partie différente du programme à un instant donné.

Nous avons vu comment un programme peut créer un processus fils. Celui-ci exécute immédiatement le programme de son père, la mémoire virtuelle, les descripteurs de fichiers, etc. de son père étant copiés. Le processus fils peut modifier sa mémoire, fermer les descripteurs de fichiers sans que cela affecte son père, et vice versa. Lorsqu’un programme crée un nouveau thread, par contre, rien n’est copié. Le thread créateur et le thread créé partagent tous deux le même espace mémoire, les mêmes descripteurs de fichiers et autres ressources. Si un thread modifie la valeur d’une variable, par exemple, l’autre thread verra la valeur modifiée. De même, si un thread ferme un descripteur de fichier, les autres threads ne peuvent plus lire ou écrire dans ce fichier. Comme un processus et tous ses threads ne peuvent exécuter qu’un seul programme à la fois, si un thread au sein d’un processus appelle une des fonctions exec, tous les autres threads se terminent (le nouveau programme peut, bien sûr, créer de nouveaux threads).

GNU/Linux implémente l’API de threading standard POSIX (appelée aussi pthreads). Toutes les fonctions et types de données relatifs aux threads sont déclarés dans le fichier d’entête <pthread.h>. Les fonctions de pthread ne font pas partie de la bibliothèque standard du C. Elles se trouvent dans libpthread, vous devez donc ajouter -lpthread sur la ligne de commande lors de l’édition de liens de votre programme.

Création de threads

Chaque thread d’un processus est caractérisé par un identifiant de thread. Lorsque vous manipulez les identifiants de threads dans des programmes C ou C++, veillez à utiliser le type pthread_t.

Lors de sa création, chaque thread exécute une fonction de thread. Il s’agit d’une fonction ordinaire contenant le code que doit exécuter le thread. Lorsque la fonction se termine, le thread se termine également. Sous GNU/Linux, les fonctions de thread ne prennent qu’un seul paramètre de type void* et ont un type de retour void*. Ce paramètre est l’argument de thread: GNU/Linux passe sa valeur au thread sans y toucher. Votre programme peut utiliser ce paramètre pour passer des données à un nouveau thread. De même, il peut utiliser la valeur de retour pour faire en sorte que le thread renvoie des données à son créateur lorsqu’il se termine.

La fonction pthread_create crée un nouveau thread. Voici les paramètres dont elle a besoin:

  1. Un pointeur vers une variable pthread_t, dans laquelle l’identifiant du nouveau thread sera stocké;
  2. Un pointeur vers un objet d’attribut de thread. Cet objet contrôle les détails de l’interaction du thread avec le reste du programme. Si vous passez NULL comme argument de thread, le thread est créé avec les attributs par défaut. Ceux-ci sont traités dans la Section 4.1.5, «Attributs de Threads»;
  3. Un pointeur vers la fonction de thread. Il s’agit d’un pointeur de fonction ordinaire de type: void* (*) (void*);
  4. item Une valeur d’argument de thread de type void*. Quoi que vous passiez, l’argument est simplement transmis à la fonction de thread lorsque celui-ci commence à s’exécuter.

Un appel à pthread_create se termine immédiatement et le thread original continue à exécuter l’instruction suivant l’appel. Pendant ce temps, le nouveau thread débute l’exécution de la fonction de thread. Linux ordonnance les deux threads de manière asynchrone et votre programme ne doit pas faire d’hypothèse sur l’ordre d’exécution relatif des instructions dans les deux threads.

Le programme du Listing !!threadcreate!! crée un thread qui affiche x de façon continue sur la sortie des erreurs. Après l’appel de pthread_create, le thread principal affiche des o indéfiniment sur la sortie des erreurs.

Listing (thread-create.c) Créer un thread

#include <pthread.h>
#include <stdio.h>
/* Affiche des x sur stderr.  Paramètre inutilisé. Ne finit jamais. */
void* print_xs (void* unused)
{
  while (1)
    fputc ('x', stderr);
  return NULL;
}
/* Le programme principal.  */
int main ()
{
  pthread_t thread_id;
  /* Crée un nouveau thread. Le nouveau thread exécutera la fonction
     print_xs. */
  pthread_create (&thread_id, NULL, &print_xs, NULL);
  /* Affiche des o en continue sur stderr. */
  while (1)
    fputc ('o', stderr);
  return 0;
}

Compilez ce programme en utilisant la commande suivante:

% cc -o thread-create thread-create.c -lpthread

Essayez de le lancer pour voir ce qui se passe. Notez que le motif formé par les x et les o est imprévisible car Linux passe la main alternativement aux deux threads.

Dans des circonstances normales, un thread peut se terminer de deux façons. La première, illustrée précédemment, est de terminer la fonction de thread. La valeur de retour de la fonction de thread est considérée comme la valeur de retour du thread. Un thread peut également se terminer explicitement en appelant pthread_exit. Cette fonction peut être appelée depuis la fonction de thread ou depuis une autre fonction appelée directement ou indirectement par la fonction de thread. L’argument de pthread_exit est la valeur de retour du thread.

Transmettre des données à un thread

L’argument de thread est une méthode pratique pour passer des données à un thread. Comme son type est void*, cependant, vous ne pouvez pas passer beaucoup de données directement en l’utilisant. Au lieu de cela, utilisez l’argument de thread pour passer un pointeur vers une structure ou un tableau de données. Une technique couramment utilisée est de définir une structure de données pour chaque argument de thread, qui contient les paramètres attendus par la fonction de thread.

En utilisant l’argument de thread, il est facile de réutiliser la même fonction pour différents threads. Ils exécutent alors tous les mêmes traitements mais sur des données différentes.

Le programme du Listing !!threadcreate2!! est similaire à l’exemple précédent. Celui-ci crée deux nouveaux threads, l’un affiche des x et l’autre des o. Au lieu de les afficher indéfiniment, cependant, chaque thread affiche un nombre prédéterminé de caractères puis se termine en sortant de la fonction de thread. La même fonction de thread, char_print, est utilisée par les deux threads mais chacun est configuré différemment en utilisant une struct char_print_parms.

Listing (thread-create2.c) Créer Deux Threads

#include <pthread.h>
#include <stdio.h>
/* Paramètres de la fonction print.   */
struct char_print_parms
{
   /* Caractère à afficher. */
   char character;
   /* Nombre de fois où il doit être affiché. */
   int count;
};
/* Affiche un certain nombre de caractères sur stderr, selon le contenu de 
    PARAMETERS, qui est un pointeur vers une struct char_print_parms. */
void* char_print (void* parameters)
{
   /* Effectue un transtypage du pointeur void vers le bon type. */
   struct char_print_parms* p = (struct char_print_parms*) parameters;
   int i;
   for (i = 0; i < p->count; ++i)
     fputc (p->character, stderr);
   return NULL;
}
/* Programme principal.   */
int main ()
{
   pthread_t thread1_id;
   pthread_t thread2_id;
   struct char_print_parms thread1_args;
   struct char_print_parms thread2_args;
   /* Crée un nouveau thread affichant 30 000 'x'. */
   thread1_args.character = 'x';
   thread1_args.count = 30000;
   pthread_create (&thread1_id, NULL, &char_print, &thread1_args);
   /* Crée un nouveau thread affichant 20 000 'o'. */
   thread2_args.character = 'o';
   thread2_args.count = 20000;
   pthread_create (&thread2_id, NULL, &char_print, &thread2_args);
   return 0;
}

Mais attendez! Le programme du Listing !!threadcreate2!! est sérieusement bogué. Le thread principal (qui exécute la fonction main) crée les structures passées en paramètre aux threads (thread1_args et thread2_args) comme des variables locales puis transmet des pointeurs sur ces structures aux threads qu’il crée. Qu’est-ce qui empêche Linux d’ordonnancer les trois threads de façon à ce que main termine son exécution avant que l’un des deux autres threads s’exécutent? Rien! Mais si cela se produit, la mémoire contenant les structures de paramètres des threads sera libérée alors que les deux autres threads tentent d’y accéder.

Synchroniser des threads

Une solution possible est de forcer main à attendre la fin des deux autres threads. Ce dont nous avons besoin est une fonction similaire à wait qui attende la fin d’un thread au lieu de celle d’un processus. Cette fonction est pthread_join, qui prend deux arguments: l’identifiant du thread à attendre et un pointeur vers une variable void* qui recevra la valeur de retour du thread s’étant terminé. Si la valeur de retour du thread ne vous est pas utile, passez NULL comme second argument.

Le Listing !!threadcreate2a!! montre la fonction main sans le bogue du Listing !!threadcreate2!!. Dans cette version, main ne se termine pas jusqu’à ce que les deux threads affichant des o et des x se soient eux-même terminés, afin qu’ils n’utilisent plus les structures contenant les arguments.

Listing (thread-create2a.c) Fonction main de thread-create2.c corrigée

int main ()
{
  pthread_t thread1_id;
  pthread_t thread2_id;
  struct char_print_parms thread1_args;
  struct char_print_parms thread2_args;
  /* Crée un nouveau thread affichant 30 000 'x'. */
  thread1_args.character = 'x';
  thread1_args.count = 30000;
  pthread_create (&thread1_id, NULL, &char_print, &thread1_args);
  /* Crée un nouveau thread affichant 20 000 'o'. */
  thread2_args.character = 'o';
  thread2_args.count = 20000;
  pthread_create (&thread2_id, NULL, &char_print, &thread2_args);
  /* S'assure que le premier thread est terminé. */
  pthread_join (thread1_id, NULL);
  /* S'assure que le second thread est terminé. */
  pthread_join (thread2_id, NULL);
  /* Nous pouvons maintenant quitter en toute sécurité.  */
  return 0;
}

Morale de l’histoire: assurez vous que toute donnée que vous passez à un thread par référence n’est pas libérée, même par un thread différent, à moins que vous ne soyez sûr que le thread a fini de l’utiliser. Cela s’applique aux variables locales, qui sont libérées lorsqu’elles sont hors de portée, ainsi qu’aux variables allouées dans le tas, que vous libérez en appelant free (ou en utilisant delete en C++).

Valeurs de retour des threads

Si le second argument que vous passez à pthread_join n’est pas NULL, la valeur de retour du thread sera stockée à l’emplacement pointé par cet argument. La valeur de retour du thread, comme l’argument de thread, est de type void*. Si vous voulez renvoyer un simple int ou un autre petit nombre, vous pouvez le faire facilement est convertissant la valeur en void* puis en le reconvertissant vers le type adéquat après l’appel de pthread_join2).

Le programme du Listing !!primes!! calcule le nième nombre premier dans un thread distinct. Ce thread renvoie le numéro du nombre premier demandé via sa valeur de retour de thread. Le thread principal, pendant ce temps, est libre d’exécuter d’autres traitements. Notez que l’algorithme de divisions successives utilisé dans compute_prime est quelque peu inefficace; consultez un livre sur les algorithmes numériques si vous avez besoin de calculer beaucoup de nombres premiers dans vos programmes.

Listing (primes.c) Calcule des Nombres Premiers dans un Thread

#include <pthread.h>
#include <stdio.h>
/* Calcules des nombres premiers successifs (très inefficace). Renvoie
   le Nième nombre premier où N est la valeur pointée par *ARG. */
void* compute_prime (void* arg)
{
  int candidate = 2;
  int n = *((int*) arg);
  while (1) {
    int factor;
    int is_prime = 1;
    /* Teste si le nombre est premier par divisions successives. */
    for (factor = 2; factor < candidate; ++factor)
      if (candidate % factor == 0) {
        is_prime = 0;
        break;
      }
    /* Est-ce le nombre premier que nous cherchons ? */
    if (is_prime) {
      if (--n == 0)
    /* Renvoie le nombre premier désiré via la valeur de retour du thread. */
        return (void*) candidate;
    }
    ++candidate;
  }
  return NULL;
}
int main ()
{
  pthread_t thread;
  int which_prime = 5000;
  int prime;
  /* Démarre le thread de calcul jusqu'au 5 000ème nombre premier. */
  pthread_create (&thread, NULL, &compute_prime, &which_prime);
  /* Faire autre chose ici... */
  /* Attend la fin du thread de calcul et récupère le résultat. */
  pthread_join (thread, (void*) &prime);
  /* Affiche le nombre premier calculé. */
  printf("Le %dème nombre premier est %d.\n", which_prime, prime);
  return 0;
}

Plus d'informations sur les identifiants de thread

De temps à autre, il peut être utile pour une portion de code de savoir quel thread l’exécute. La fonction pthread_self renvoie l’identifiant du thread depuis lequel elle a été appelée. Cet identifiant de thread peut être comparé à un autre en utilisant la fonction pthread_equal.

Ces fonctions peuvent être utiles pour déterminer si un identifiant de thread particulier correspond au thread courant. Par exemple, un thread ne doit jamais appeler pthread_join pour s’attendre lui-même (dans ce cas, pthread_join renverrait le code d’erreur EDEADLK). Pour le vérifier avant l’appel, utilisez ce type de code :

if (!pthread_equal (pthread_self (), other_thread))
  pthread_join (other_thread, NULL);

Attributs de thread

Les attributs de thread proposent un mécanisme pour contrôler finement le comportement de threads individuels. Souvenez-vous que pthread_create accepte un argument qui est un pointeur vers un objet d’attributs de thread. Si vous passez un pointeur nul, les attributs par défaut sont utilisés pour configurer le nouveau thread. Cependant, vous pouvez créer et personnaliser un objet d’attributs de threads pour spécifier vos propres valeurs.

Pour indiquer des attributs de thread personnalisés, vous devez suivre ces étapes :

  1. Créez un objet pthread_attr_t. La façon la plus simple de le faire est de déclarer une variable automatique de ce type.
  2. Appelez pthread_attr_init en lui passant un pointeur vers cet objet. Cela initialise les attributs à leur valeur par défaut.
  3. Modifiez l’objet d’attributs pour qu’ils contiennent les valeurs désirées.
  4. Passez un pointeur vers l’objet créé lors de l’appel à pthread_create.
  5. Appelez pthread_attr_destroy pour libérer l’objet d’attributs. La variable pthread_attr_t n’est pas libérée en elle-même; elle peut être réinitialisée avec pthread_attr_init.

Un même objet d’attributs de thread peut être utilisé pour démarrer plusieurs threads. Il n’est pas nécessaire de conserver l’objet d’attributs de thread une fois qu’ils ont été créés.

Pour la plupart des applications GNU/Linux, un seul attribut de thread a généralement de l’intérêt (les autres attributs disponibles sont principalement destinés à la programmation en temps réel). Cet attribut est l’état de détachement (detach state) du thread. Un thread peut être un thread joignable (par défaut) ou un thread détaché. Les ressources d’un thread joignable, comme pour un processus, ne sont pas automatiquement libérées par GNU/Linux lorsqu’il se termine. Au lieu de cela, l’état de sortie du thread reste dans le système (un peu comme pour un processus zombie) jusqu’à ce qu’un autre thread appelle pthread_join pour récupérer cette valeur de retour. Alors seulement, les ressources sont libérées. Les ressources d’un thread détaché, au contraire, sont automatiquement libérées lorsqu’il se termine. Cela implique qu’un autre thread ne peut attendre qu’il se termine avec pthread_join ou obtenir sa valeur de retour.

Pour paramétrer l’état de détachement dans un objet d’attributs de thread, utilisez pthread_attr_setdetachstate. Le premier argument est un pointeur vers l’objet d’attributs de thread et le second est l’état désiré. Comme l’état joignable est la valeur par défaut, il n’est nécessaire d’effectuer cet appel que pour créer des threads détachés; passez PTHREAD_CREATE_DETACHED comme second argument.

Le code du Listing !!detached!! crée un thread détaché en activant l’attribut de détachement de thread:

Listing (detached.c) Programme Squelette Créant un Thread Détaché

#include <pthread.h>
void* thread_function (void* thread_arg)
{
  /* Effectuer les traitements ici... */
}
int main ()
{
  pthread_attr_t attr;
  pthread_t thread;
  pthread_attr_init (&attr);
  pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
  pthread_create (&thread, &attr, &thread_function, NULL);
  pthread_attr_destroy (&attr);
  /* Effectuer les traitements ici...  */
  /* Pas besoin d'attendre le deuxième thread.  */
  return 0;
}

Même si un thread est créé dans un état joignable, il peut être transformé plus tard en un thread détaché. Pour cela, appelez pthread_detach. Une fois qu’un thread est détaché, il ne peut plus être rendu joignable.

Annulation de thread

Dans des circonstances normales, un thread se termine soit en arrivant à la fin de la fonction de thread, soit en appelant la fonction pthread_exit. Cependant, il est possible pour un thread de demander la fin d’un autre thread. Cela s’appelle annuler un thread.

Pour annuler un thread, appelez pthread_cancel, en lui passant l’identifiant du thread à annuler. Un thread annulé peut être joint ultérieurement; en fait, vous devriez joindre un thread annulé pour libérer ses ressources, à moins que le thread ne soit détaché (consultez la Section 4.1.5, « Attributs de Thread »). La valeur de retour d’un thread annulé est la valeur spéciale PTHREAD_CANCELED.

Souvent un thread peut être en train d’exécuter un code qui doit être exécuté totalement ou pas du tout. Par exemple, il peut allouer des ressources, les utiliser, puis les libérer. Si le thread est annulé au milieu de ce code, il peut ne pas avoir l’opportunité de libérer les ressources et elles seront perdues. Pour éviter ce cas de figure, un thread peut contrôler si et quand il peut être annulé.

Un thread peut se comporter de trois façons face à l’annulation.

  • Il peut être annulable de façon asynchrone. C’est-à-dire qu’il peut être annulé à n’importe quel moment de son exécution.
  • Il peut être annulable de façon synchrone. Le thread peut être annulé, mais pas n’importe quand pendant son exécution. Au lieu de cela, les requêtes d’annulation sont mises en file d’attente et le thread n’est annulé que lorsqu’il atteint un certain point de son exécution.
  • Il peut être impossible à annuler. Les tentatives d’annulation sont ignorées silencieusement.

Lors de sa création, un thread est annulable de façon synchrone.

Threads synchrones et asynchrones

Un thread annulable de façon asynchrone peut être annulé à n’importe quel point de son exécution. Un thread annulable de façon synchrone, par contre, ne peut être annulé qu’à des endroits précis de son exécution. Ces endroits sont appelés points d’annulation. Le thread mettra les requêtes d’annulation en attente jusqu’à ce qu’il atteigne le point d’annulation suivant.

Pour rendre un thread annulable de façon asynchrone, utilisez pthread_setcanceltype. Cela n’affecte que le thread effectuant l’appel. Le premier argument doit être PTHREAD_CANCEL_ASYNCHRONOUS pour rendre le thread annulable de façon asynchrone, ou PTHREAD_CANCEL_DEFERRED pour repasser immédiatement en état d’annulation synchrone. Le second argument, s’il n’est pas NULL, est un pointeur vers une variable qui recevra le type d’annulation précédemment supportée par le thread. Cet appel, par exemple, rend le thread appelant annulable de façon asynchrone.

pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

A quoi ressemble un point d’annulation et où doit-il être placé? La façon la plus directe de créer un point d’annulation est d’appeler pthread_testcancel. Cette fonction ne fait rien à part traiter une annulation en attente dans un thread annulable de façon synchrone. Vous devriez appeler pthread_testcancel périodiquement durant des calculs longs au sein d’une fonction de thread, aux endroits où le thread peut être annulé sans perte de ressources ou autres effets de bord.

Un certain nombre d’autres fonctions sont également des points d’annulation implicites. Elles sont listées sur la page de manuel de pthread_cancel. Notez que d’autres fonctions peuvent y faire appel en interne et donc constituer des points d’annulation implicites.

Sections critiques non-annulables

Un thread peut désactiver son annulation avec la fonction pthread_setcancelstate. Comme pthread_setcanceltype, elle affecte le thread appelant. Le premier argument est PTHREAD_CANCEL_DISABLE pour désactiver l’annulation, ou PTHREAD_CANCEL_ENABLE pour réactiver l’annulation. Le second argument, s’il n’est pas NULL, pointe vers une variable qui recevra l’état précédent de l’annulation. Cet appel, par exemple, désactive l’annulation du thread appelant.

pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, NULL);

L’utilisation de pthread_setcancelstate vous permet d’implémenter des sections critiques. Une section critique est une séquence de code qui doit être exécutée entièrement ou pas du tout; en d’autres termes, si un thread commence l’exécution d’une section critique, il doit atteindre la fin de la section critique sans être annulé.

Par exemple, supposons que vous écriviez une routine pour un programme bancaire qui transfère de l’argent d’un compte à un autre. Pour cela, vous devez ajouter la valeur au solde d’un compte et la soustraire du solde de l’autre. Si le thread exécutant votre routine est annulé entre les deux opérations, le programme aura augmenté de façon incorrecte l’argent détenu par la banque en ne terminant pas la transaction. Pour éviter cela, placez les deux opérations au sein d’une section critique.

Vous pourriez implémenter le transfert avec une fonction comme process_transaction présentée dans le Listing !!criticalsection!!. Cette fonction désactive l’annulation de thread pour démarrer une section critique avant toute modification de solde.

Listing (critical-section.c) Transaction avec Section Critique

#include <pthread.h>
#include <stdio.h>
#include <string.h>
/* Tableau des soldes de comptes, indexé par numéro de compte.  */
float* account_balances;
/* Transfère DOLLARS depuis le compte FROM_ACCT vers TO_ACCT. Renvoie
   0 si la transaction s'est bien déroulée ou 1 si le solde de FROM_ACCT
   est trop faible. */
int process_transaction (int from_acct, int to_acct, float dollars)
{
  int old_cancel_state;
  /* Vérifie le solde de FROM_ACCT. */
  if (account_balances[from_acct] < dollars)
    return 1;
  /* Début de la section critique. */
  pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &old_cancel_state);
  /* Transfère l'argent. */
  account_balances[to_acct] += dollars;
  account_balances[from_acct] -= dollars;
  /* Fin de la section critique. */
  pthread_setcancelstate (old_cancel_state, NULL);
  return 0;
}

Notez qu’il est important de restaurer l’ancien état de l’annulation à la fin de la section critique plutôt que de le positionner systématiquement à PTHREAD_CANCEL_ENABLE. Cela vous permet d’appeler la fonction process_transaction en toute sécurité depuis une autre section critique − dans ce cas, votre fonction remettrait l’état de l’annulation à la même valeur que lorsqu’elle a débuté.

Quand utiliser l'annulation de thread ?

En général, ce n’est pas une bonne idée d’utiliser l’annulation de thread pour en terminer l’exécution, excepté dans des circonstances anormales. En temps normal, il est mieux d’indiquer au thread qu’il doit se terminer, puis d’attendre qu’il se termine de lui-même. Nous reparlerons des techniques de communication avec les threads plus loin dans ce chapitre et dans le Chapitre 5, « Communication Interprocessus ».

Données propres à un thread

Contrairement aux processus, tous les threads d’un même programme partagent le même espace d’adressage. Cela signifie que si un thread modifie une valeur en mémoire (par exemple, une variable globale), cette modification est visible dans tous les autres threads. Cela permet à plusieurs threads de manipuler les mêmes données sans utiliser les mécanismes de communication interprocessus (décrits au Chapitre 5).

Néanmoins, chaque thread a sa propre pile d’appel. Cela permet à chacun d’exécuter un code différent et d’utiliser des sous-routines de façon classique. Comme dans un programme monothreadé, chaque invocation de sous-routine dans chaque thread a son propre jeu de variables locales, qui sont stockées dans la pile de ce thread.

Cependant, il peut parfois être souhaitable de dupliquer certaines variables afin que chaque thread dispose de sa propre copie. GNU/Linux supporte cette fonctionnalité avec une zone de données propres au thread. Les variables stockées dans cette zone sont dupliquées pour chaque thread et chacun peut modifier sa copie sans affecter les autres. Comme tous les threads partagent le même espace mémoire, il n’est pas possible d’accéder aux données propres à un thread via des références classiques. GNU/Linux fournit des fonctions dédiées pour placer des valeurs dans la zone de données propres au thread et les récupérer.

Vous pouvez créer autant d’objets de données propres au thread que vous le désirez, chacune étant de type void*. Chaque objet est référencé par une clé. Pour créer une nouvelle clé, et donc un nouvel objet de données pour chaque thread, utilisez pthread_key_create. Le premier argument est un pointeur vers une variable pthread_key_t. Cette clé peut être utilisée par chaque thread pour accéder à sa propre copie de l’objet de données correspondant. Le second argument de pthread_key_create est une fonction de libération des ressources. Si vous passez un pointeur sur une fonction, GNU/Linux appelle celle-ci automatiquement lorsque chaque thread se termine en lui passant la valeur de la donnée propre au thread correspondant à la clé. Ce mécanisme est très pratique car la fonction de libération des ressources est appelée même si le thread est annulé à un moment quelconque de son exécution. Si la valeur propre au thread est nulle, la fonction de libération de ressources n’est pas appelée. Si vous n’avez pas besoin de fonction de libération de ressources, vous pouvez passer NULL au lieu d’un pointeur de fonction.

Une fois que vous avez créé une clé, chaque thread peut positionner la valeur correspondant à cette clé en appelant pthread_setspecific. Le premier argument est la clé, et le second est la valeur propre au thread à stocker sous forme de void*. Pour obtenir un objet de données propre au thread, appelez pthread_getspecific, en lui passant la clé comme argument.

Supposons, par exemple, que votre application partage une tâche entre plusieurs threads. À des fins d’audit, chaque thread doit avoir un fichier journal séparé dans lequel des messages d’avancement pour ce thread sont enregistrés. La zone de données propres au thread est un endroit pratique pour stocker le pointeur sur le fichier journal dans chaque thread.

Le Listing !!tsd!! présente une implémentation d’un tel mécanisme. La fonction main de cet exemple crée une clé pour stocker le pointeur vers le fichier propre à chaque thread puis la stocke au sein de la variable globale thread_lock_key. Comme il s’agit d’une variable globale, elle est partagée par tous les threads. Lorsqu’un thread commence à exécuter sa fonction, il ouvre un fichier journal et stocke le pointeur de fichier sous cette clé. Plus tard, n’importe lequel de ces thread peut appeler write_to_thread_log pour écrire un message dans le fichier journal propre au thread. Cette fonction obtient le pointeur de fichier du fichier log du thread depuis les données propres au thread et y écrit le message.

Listing (tsd.c) Fichiers Logs par Thread avec Données Propres au Thread

#include <malloc.h>
#include <pthread.h>
#include <stdio.h>
/* Clé utilisée pour associer un pointeur de fichier à chaque thread. */
static pthread_key_t thread_log_key;
/* Écrit MESSAGE vers le fichier journal du thread courant. */
void write_to_thread_log (const char* message)
{
  FILE* thread_log = (FILE*) pthread_getspecific (thread_log_key);
  fprintf (thread_log, "%s\n", message);
}
/* Ferme le pointeur vers le fichier journal THREAD_LOG.  */
void close_thread_log (void* thread_log){
  fclose ((FILE*) thread_log);
}
void* thread_function (void* args)
{
  char thread_log_filename[20];
  FILE* thread_log;
  /* Génère le nom de fichier pour le fichier journal de ce thread. */
  sprintf (thread_log_filename, "thread%d.log", (int) pthread_self ());
  /* Ouvre le fichier journal. */
  thread_log = fopen (thread_log_filename, "w");
  /* Stocke le pointeur de fichier dans les données propres au thread sous
     la clé thread_log_key. */
  pthread_setspecific (thread_log_key, thread_log);
  write_to_thread_log ("Démarrage du Thread.");
  /* Placer les traitements ici... */
  return NULL;
}
int main ()
{
  int i;
  pthread_t threads[5];
  /* Crée une clé pour associer les pointeurs de fichier journal dans les
     données propres au thread. Utilise close_thread_log pour libérer
     les pointeurs de fichiers. */
  pthread_key_create (&thread_log_key, close_thread_log);
  /* Crée des threads pour effectuer les traitements. */
  for (i = 0; i < 5; ++i)
    pthread_create (&(threads[i]), NULL, thread_function, NULL);
  /* Attend la fin de tous les threads. */
  for (i = 0; i < 5; ++i)
    pthread_join (threads[i], NULL);
  return 0;
}

Remarquez que thread_function n’a pas besoin de fermer le fichier journal. En effet, lorsque la clé du fichier a été créée, close_thread_log a été spécifiée comme fonction de libération de ressources pour cette clé. Lorsqu’un thread se termine, GNU/Linux appelle cette fonction, en lui passant la valeur propre au thread correspondant à la clé du fichier journal. Cette fonction prend soin de fermer le fichier journal.

Gestionnaires de libération de ressources

Les fonctions de libération de ressources associées aux clés des données spécifiques au thread peuvent être très pratiques pour s’assurer que les ressources ne sont pas perdues lorsqu’un thread se termine ou est annulé. Quelquefois, cependant, il est utile de pouvoir spécifier des fonctions de libération de ressources sans avoir à créer un nouvel objet de données propre au thread qui sera dupliqué pour chaque thread. Pour ce faire, GNU/Linux propose des gestionnaires de libération de ressources.

Un gestionnaire de libération de ressources est tout simplement une fonction qui doit être appelée lorsqu’un thread se termine. Le gestionnaire prend un seul paramètre void* et la valeur de l’argument est spécifiée lorsque le gestionnaire est enregistré − cela facilite l’utilisation du même gestionnaire pour libérer plusieurs instances de ressources.

Un gestionnaire de libération de ressources est une mesure temporaire, utilisé pour libérer une ressource uniquement si le thread se termine ou est annulé au lieu de terminer l’exécution d’une certaine portion de code. Dans des circonstances normales, lorsqu’un thread ne se termine pas et n’est pas annulé, la ressource doit être libérée explicitement et le gestionnaire de ressources supprimé.

Pour enregistrer un gestionnaire de libération de ressources, appelez pthread_cleanup_push, en lui passant un pointeur vers la fonction de libération de ressources et la valeur de son argument void*. L’appel à pthread_cleanup_push doit être contrebalancé avec un appel à pthread_cleanup_pop qui supprime le gestionnaire de libération de ressources. Dans une optique de simplification, pthread_cleanup_pop prend un indicateur int en argument; s’il est différent de zéro, l’action de libération de ressources est exécutée lors de la suppression.

L’extrait de programme du Listing !!cleanup!! montre comment vous devriez utiliser un gestionnaire de libération de ressources pour vous assurer qu’un tampon alloué dynamiquement est libéré si le thread se termine.

Listing (cleanup.c) Extrait de Programme Montrant l’Utilisation d’un Gestionnaire de Libération de Ressources

#include <malloc.h>
#include <pthread.h>
/* Alloue un tampon temporaire.  */
void* allocate_buffer (size_t size)
{
  return malloc (size);
}
/* Libère un tampon temporaire.  */
void deallocate_buffer (void* buffer)
{
  free (buffer);
}
void do_some_work ()
{
  /* Alloue un tampon temporaire.  */
  void* temp_buffer = allocate_buffer (1024);
  /* Enregistre un gestionnaire de libération de ressources pour ce tampon 
     pour le libérer si le thread se termine ou est annulé. */
  pthread_cleanup_push (deallocate_buffer, temp_buffer);
  /* Placer ici des traitements qui pourraient appeler pthread_exit ou être
     annulés... */
  /* Supprime le gestionnaire de libération de ressources. Comme nous passons 
     une valeur différente de zéro, la libération est effectuée
     par l'appel de deallocate_buffer. */
  pthread_cleanup_pop (1);
}

Comme l’argument de pthread_cleanup_pop est différent de zéro dans ce cas, la fonction de libération de ressources deallocate_buffer est appelée automatiquement et il n’est pas nécessaire de le faire explicitement. Dans ce cas simple, nous aurions pu utiliser la fonction de la bibliothèque standard free comme fonction de libération de ressources au lieu de deallocate_buffer.

Libération de ressources de thread en C++

Les programmeurs C++ sont habitués à avoir des fonctions de libération de ressources «gratuitement» en plaçant les actions adéquates au sein de destructeurs. Lorsque les objets sont hors de portée, soit à cause de la fin d’un bloc, soit parce qu’une exception est lancée, le C++ s’assure que les destructeurs soient appelés pour les variables automatiques qui en ont un. Cela offre un mécanisme pratique pour s’assurer que le code de libération de ressources est appelé quelle que soit la façon dont le bloc se termine.

Si un thread appelle pthread_exit, cependant, le C++ ne garantit pas que les destructeurs soient appelés pour chacune des variables automatiques situées sur la pile du thread. Une façon intelligente d’obtenir le même comportement est d’invoquer pthread_exit au niveau de la fonction de thread en lançant une exception spéciale.

Le programme du Listing !!cxxexit!! démontre cela. Avec cette technique, une fonction indique son envie de quitter le thread en lançant une ThreadExitException au lieu d’appeler pthread_exit directement. Comme l’exception est interceptée au niveau de la fonction de thread, toutes les variables locales situées sur la pile du thread seront détruites proprement lorsque l’exception est remontée.

Listing (cxx-exit.cpp) Implémenter une Sortie de Thread en C++

#include <pthread.h>
class ThreadExitException
{
public:
  /* Crée une exception signalant la fin d'un thread avec RETURN_VALUE. */
  ThreadExitException (void* return_value)
    : thread_return_value_ (return_value)   {
   }
   /* Quitte le thread en utilisant la valeur de retour fournie dans le 
      constructeur. */
   void* DoThreadExit ()
   {
     pthread_exit (thread_return_value_);
   }
private:
   /* Valeur de retour utilisée lors de la sortie du thread. */
   void* thread_return_value_;
};
void do_some_work ()
{
   while (1) {
     /* Placer le code utile ici... */
     if (should_exit_thread_immediately ())
       throw ThreadExitException (/* thread?s return value = */ NULL);
   }
}
void* thread_function (void*)
{
  try {
    do_some_work ();
  }
  catch (ThreadExitException ex) {
    /* Une fonction a signalé que l'on devait quitter le thread. */
    ex.DoThreadExit ();
  }
  return NULL;
}

Synchronisation et sections critiques

Programmer en utilisant les threads demande beaucoup de rigueur car la plupart des programmes utilisant les threads sont des programmes concurrents. En particulier, il n’y a aucun moyen de connaître la façon dont seront ordonnancés les threads les uns par rapport aux autres. Un thread peut s’exécuter pendant longtemps ou le système peut basculer d’un thread à un autre très rapidement. Sur un système avec plusieurs processeurs, le système peut même ordonnancer plusieurs threads afin qu’ils s’exécutent physiquement en même temps.

Déboguer un programme qui utilise les threads est compliqué car vous ne pouvez pas toujours reproduire facilement le comportement ayant causé le problème. Vous pouvez lancer le programme une première fois sans qu’aucun problème ne survienne et la fois suivante, il plante. Il n’y a aucun moyen de faire en sorte que le système ordonnance les threads de la même façon d’une fois sur l’autre.

La cause la plus vicieuse de plantage des programmes utilisant les threads est lorsque ceux-ci tentent d’accéder aux mêmes données. Comme nous l’avons dit précédemment, il s’agit d’un des aspects les plus puissants des threads mais cela peut également être dangereux. Si un thread n’a pas terminé la mise à jour d’une structure de données lorsqu’un autre thread y accède, il s’en suit une situation imprévisible. Souvent, les programmes bogués qui utilisent les threads contiennent une portion de code qui ne fonctionne que si un thread a la main plus souvent − ou plus tôt − qu’un autre. Ces bogues sont appelés conditions de concurrence critique; les threads sont en concurrence pour modifier la même structure de données.

Conditions de concurrence critique

Supposons que votre programme ait une série de tâches en attente traitées par plusieurs threads concurrents. La file d’attente des tâches est représentée par une liste chaînée d’objets struct job.

Après que chaque thread a fini une opération, il vérifie la file pour voir si une nouvelle tâche est disponible. Si job_queue n’est pas NULL, le thread supprime la tête de la liste chaînée et fait pointer job_queue vers la prochaine tâche de la liste.

La fonction de thread qui traite les tâches de la liste pourrait ressembler au Listing !!jobqueue1!!.

Listing (job-queue1.c) Fonction de Thread Traitant une File de Tâches

#include <malloc.h>
struct job {
   /* Champ de chaînage.  */
   struct job* next;
   /* Autres champs décrivant la tâche... */
};
/* Liste chaînée de tâches en attente. */
struct job* job_queue;
/* Traite les tâches jusqu'à ce que la file soit vide. */
void* thread_function (void* arg)
{
   while (job_queue != NULL) {
     /* Récupère la tâche suivante. */
     struct job* next_job = job_queue;
     /* Supprime cette tâche de la liste. */
     job_queue = job_queue->next;
     /* Traite la tâche. */
     process_job (next_job);
     /* Libération des ressources. */
     free (next_job);
   }
   return NULL;
}

Supposons maintenant que deux threads finissent une tâche à peu près au même moment, mais qu’il ne reste qu’une seule tâche dans la liste. Le premier thread regarde si job_queue est nul; comme ce n’est pas le cas, le thread entre dans la boucle et stocke le pointeur vers la tâche dans next_job. À ce moment, Linux interrompt le thread et passe la main au second. Le second thread vérifie également job_queue, comme elle n’est pas NULL, affecte la même valeur que le premier à next_job. Par une malheureuse coïncidence, nous avons deux threads exécutant la même tâche.

Pire, un des thread va positionner job_queue à NULL pour supprimer l’objet de la liste. Lorsque l’autre évaluera job_queue→next, il en résultera une erreur de segmentation.

C’est un exemple de condition de concurrence critique. En d’« heureuses » circonstances, cet ordonnancement particulier des threads ne surviendra jamais et la condition de concurrence critique ne sera jamais révélée. Dans des circonstances différentes, par exemple dans le cas d’un système très chargé (ou sur le nouveau serveur multiprocesseur d’un client important!) le bogue peut apparaître.

Pour éliminer les conditions de concurrence critique, vous devez trouver un moyen de rendre les opérations atomiques. Une opération atomique est indivisible et impossible à interrompre; une fois qu’elle a débuté, elle ne sera pas suspendue ou interrompue avant d’être terminée, et aucune autre opération ne sera accomplie pendant ce temps. Dans notre exemple, nous voulons vérifier la valeur de job_queue et si elle n’est pas NULL, supprimer la première tâche, tout cela en une seule opération atomique.

Mutexes

La solution pour notre problème de concurrence critique au niveau de la file de tâches est de n’autoriser qu’un seul thread à accéder à la file. Une fois que le thread commence à observer la file d’attente, aucun autre thread ne doit pouvoir y accéder jusqu’à ce que le premier ait décidé s’il doit traiter une tâche, et si c’est le cas, avant qu’il n’ait supprimé la tâche de la liste.

L’implémentation de ce mécanisme requiert l’aide du système d’exploitation. GNU/Linux propose des mutexes, raccourcis de MUTual EXclusion locks (verrous d’exclusion mutuelle). Un mutex est un verrouillage spécial qu’un seul thread peut utiliser à la fois. Si un thread verrouille un mutex puis qu’un second tente de verrouiller le même mutex, ce dernier est bloqué ou suspendu. Le second thread est débloqué uniquement lorsque le premier déverrouille le mutex − ce qui permet la reprise de l’exécution. GNU/Linux assure qu’il n’y aura pas de condition de concurrence critique entre deux threads tentant de verrouiller un mutex; seul un thread obtiendra le verrouillage et tous les autres seront bloqués.

On peut faire l’analogie entre un mutex et une porte de toilettes. Lorsque quelqu’un entre dans les toilettes et verrouille la porte, si une personne veut entrer dans les toilettes alors qu’ils sont occupés, elle sera obligée d’attendre dehors jusqu’à ce que l’occupant sorte.

Pour créer un mutex, créez une variable pthread_mutex_t et passez à pthread_mutex_init un pointeur sur cette variable. Le second argument de pthread_mutex_init est un pointeur vers un objet d’attributs de mutex. Comme pour pthread_create, si le pointeur est nul, ce sont les valeurs par défaut des attributs qui sont utilisées. La variable mutex ne doit être initialisée qu’une seule fois. Cet extrait de code illustre la déclaration et l’initialisation d’une variable mutex:

pthread_mutex_t mutex;
pthread_mutex_init (&mutex, NULL);

Une façon plus simple de créer un mutex avec les attributs par défaut est de l’initialiser avec la valeur spéciale PTHREAD_MUTEX_INITIALIZER. Aucun appel à pthread_mutex_init n’est alors nécessaire. Cette méthode est particulièrement pratique pour les variables globales (et les membres static en C++). L’extrait de code précédent pourrait être écrit comme suit:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

Un thread peut essayer de verrouiller un mutex en appelant pthread_mutex_lock dessus. Si le mutex était déverrouillé, il devient verrouillé et la fonction se termine immédiatement. Si le mutex était verrouillé par un autre thread, pthread_mutex_lock bloque l’exécution et ne se termine que lorsque le mutex est déverrouillé par l’autre thread. Lorsque le mutex est déverrouillé, seul un des threads suspendus (choisi aléatoirement) est débloqué et autorisé à accéder au mutex; les autres threads restent bloqués.

Un appel à pthread_mutex_unlock déverrouille un mutex. Cette fonction doit toujours être appelée par le thread qui a verrouillé le mutex.

Listing !!jobqueue!! montre une autre version de la file de tâches. Désormais, celle-ci est protégée par un mutex. Avant d’accéder à la file (que ce soit pour une lecture ou une écriture), chaque thread commence par verrouiller le mutex. Ce n’est que lorsque la séquence de vérification de la file et de suppression de la tâche est terminée que le mutex est débloqué. Cela évite l’apparition des conditions de concurrence ciritique citées précédemment.

Listing (job-queue.c) Fonction de Thread de File de Tâches, Protégée par un Mutex

#include <malloc.h>
#include <pthread.h>
struct job {
   /* Pointeur vers la tâche suivante.  */
   struct job* next;
};
/* Liste chaînée des tâches en attente.   */
struct job* job_queue;
/* Mutex protégeant la file de tâches. */
pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Traite les tâches en attente jusqu'à ce que la file soit vide.  */
void* thread_function (void* arg)
{
  while (1) {
    struct job* next_job;
    /* Verrouille le mutex de la file de tâches. */
    pthread_mutex_lock (&job_queue_mutex);
    /* Il est maintenant sans danger de vérifier si la file est vide. */
    if (job_queue == NULL)
      next_job = NULL;
    else {
      /* Récupère la tâche suivante. */
      next_job = job_queue;
      /* Supprime cette tâche de la liste. */
      job_queue = job_queue->next;
    }
    /* Déverrouille le mutex de la file de tâches car nous en avons fini avec
       la file pour l'instant. */
    pthread_mutex_unlock (&job_queue_mutex);
    /* La file était vide ? Si c'est le cas, le thread se termine.  */
    if (next_job == NULL)
      break;
    /* Traite la tâche. */
    process_job (next_job);
    /* Libération des ressources. */
    free (next_job);
  }
  return NULL;
}

Tous les accès à job_queue, le pointeur de données partagé, ont lieu entre l’appel à pthread_mutex_lock et celui à pthread_mutex_unlock. L’accès à un objet décrivant une tâche, stocké dans next_job, a lieu en dehors de cette zone uniquement une fois que l’objet a été supprimé de la liste et a donc été rendu inaccessible aux autres threads.

Notez que si la file est vide (c’est-à-dire que job_queue est NULL), nous ne quittons pas la boucle immédiatement car dans ce cas le mutex resterait verrouillé empêchant l’accès à la file de tâche par un quelconque autre thread. Au lieu de cela, nous notons que c’est le cas en mettant next_job à NULL et en ne quittant la boucle qu’après avoir déverrouillé le mutex.

L’utilisation de mutex pour verrouiller job_queue n’est pas automatique; c’est à vous d’ajouter le code pour verrouiller le mutex avant d’accéder à la variable et le déverrouiller ensuite. Par exemple, une fonction d’ajout de tâche à la file pourrait ressembler à ce qui suit:

void enqueue_job (struct job* new_job)
{
  pthread_mutex_lock (&job_queue_mutex);
  new_job->next = job_queue;
  job_queue = new_job;
  pthread_mutex_unlock (&job_queue_mutex);
}

Interblocage de mutex

Les mutexes offrent un mécanisme permettant à un thread de bloquer l’exécution d’un autre. Cela ouvre la possibilité à l’apparition d’une nouvelle classe de bogues appelés interblocages (deadlocks). Un interblocage survient lorsque un ou plusieurs threads sont bloqués en attendant quelque chose qui n’aura jamais lieu.

Un type simple d’interblocage peut survenir lorsqu’un même thread tente de verrouiller un mutex deux fois d’affilée. Ce qui se passe dans un tel cas dépend du type de mutex utilisé. Trois types de mutexes existent:

  • Le verrouillage d’un mutex rapide (le type par défaut) provoquera un interblocage. Une tentative de verrouillage sur le mutex est bloquante jusqu’à ce que le mutex soit déverrouillé. Mais comme le thread est bloqué sur un mutex qu’il a lui-même verrouillé, le verrou ne pourra jamais être supprimé.
  • Le verrouillage d’un mutex récursif ne cause pas d’interblocage. Un mutex récursif peut être verrouillé plusieurs fois par le même thread en toute sécurité. Le mutex se souvient combien de fois pthread_mutex_lock a été appelé par le thread qui détient le verrou; ce thread doit effectuer autant d’appels à pthread_mutex_unlock pour que le mutex soit effectivement déverrouillé et qu’un autre thread puisse y accéder.
  • GNU/Linux détectera et signalera un double verrouillage sur un mutex à vérification d’erreur qui causerait en temps normal un interblocage. Le second appel à pthread_mutex_lock renverra le code d’erreur EDEADLK.

Par défaut, un mutex GNU/Linux est de type rapide. Pour créer un mutex d’un autre type, commencez par créer un objet d’attributs de mutex en déclarant une variable de type pthread_mutexattr_t et appelez pthread_mutexattr_init en lui passant un pointeur dessus. Puis définissez le type de mutex en appelant pthread_mutexattr_setkind_np; son premier argument est un pointeur vers l’objet d’attributs de mutex et le second est PTHREAD_MUTEX_RECURSIVE_NP pour un mutex récursif ou PTHREAD_MUTEX_ERRORCHECK_NP pour un mutex à vérification d’erreurs. Passez un pointeur vers l’objet d’attributs de mutex à pthread_mutex_init pour créer un mutex du type désiré, puis détruisez l’objet d’attributs avec pthread_mutexattr_destroy.

Le code suivant illustre la création d’un mutex à vérification d’erreurs, par exemple:

pthread_mutexattr_t attr;
pthread_mutex_t mutex;
pthread_mutexattr_init (&attr);
pthread_mutexattr_setkind_np (&attr, PTHREAD_MUTEX_ERRORCHECK_NP);
pthread_mutex_init (&mutex, &attr);
pthread_mutexattr_destroy (&attr);

Comme le suggère le suffixe «np», les mutexes récursifs et à vérification d’erreurs sont spécifiques à GNU/Linux et ne sont pas portables. Ainsi, il est généralement déconseillé de les utiliser dans vos programmes (les mutexes à vérification d’erreurs peuvent cependant être utiles lors du débogage).

Vérification de mutex non bloquante

De temps en temps, il peut être utile de tester si un mutex est verrouillé sans être bloqué s’il l’est. Par exemple, un thread peut avoir besoin de verrouiller un mutex mais devoir faire autre chose au lieu de se bloquer si le mutex est déjà verrouillé. Comme pthread_mutex_lock ne se termine pas avant le déverrouillage du mutex, une autre fonction est nécessaire.

GNU/Linux fournit pthread_mutex_trylock pour ce genre de choses. Si vous appelez pthread_mutex_trylock sur un mutex déverrouillé, vous verrouillerez le mutex comme si vous aviez appelé pthread_mutex_lock et pthread_mutex_trylock renverra zéro. Par contre, si le mutex est déjà verrouillé par un autre mutex, pthread_mutex_trylock ne sera pas bloquante. Au lieu de cela, elle se terminera en renvoyant le code d’erreur EBUSY. Le verrou sur le mutex détenu par l’autre thread n’est pas affecté. Vous pouvez réessayer plus tard d’obtenir un verrou.

Sémaphores pour les threads

Dans l’exemple précédent, dans lequel plusieurs threads traitent les tâches d’une file, la fonction principale des threads traite la tâche suivante jusqu’à ce qu’il n’en reste plus, à ce moment, le thread se termine. Ce schéma fonctionne si toutes les tâches sont mises dans la file au préalable ou si de nouvelles tâches sont ajoutées au moins aussi vite que la vitesse de traitement des threads. Cependant, si les threads fonctionnent trop rapidement, la file de tâches se videra et les threads se termineront. Si de nouvelles tâches sont mises dans la file plus tard, il ne reste plus de thread pour les traiter. Ce que nous devrions faire est de mettre en place un mécanisme permettant de bloquer les threads lorsque la file se vide et ce jusqu’à ce que de nouvelles tâches soient disponibles.

L’utilisation de sémaphores permet de faire ce genre de choses. Un sémaphore est un compteur qui peut être utilisé pour synchroniser plusieurs threads. Comme avec les mutexes, GNU/Linux garantit que la vérification ou la modification de la valeur d’un sémaphore peut être accomplie en toute sécurité, sans risque de concurrence critique.

Chaque sémaphore dispose d’une valeur de compteur, qui est un entier positif ou nul. Un sémaphore supporte deux opérations de base:

  • Une opération d’attente (wait), qui décrémente la valeur du sémaphore d’une unité. Si la valeur est déjà à zéro, l’opération est bloquante jusqu’à ce que la valeur du sémaphore redevienne positive (en raison d’une opération de la part d’un autre thread). Lorsque la valeur du sémaphore devient positive, elle est décrémentée d’une unité et l’opération d’attente se termine.
  • Une opération de réveil (post) qui incrémente la valeur du sémaphore d’une unité. Si le sémaphore était précédemment à zéro et que d’autres threads étaient en attente sur ce même sémaphore, un de ces threads est débloqué et son attente se termine (ce qui ramène la valeur du sémaphore à zéro).

Notez que GNU/Linux fournit deux implémentations légèrement différentes des sémaphores. Celle que nous décrivons ici est l’implémentation POSIX standard. Utilisez cette implémentation lors de la communication entre threads. L’autre implémentation, utilisée pour la communication entre les processus, est décrite dans la Section 5.2, « Sémaphores pour les Processus ». Si vous utilisez les sémaphores, incluez <semaphore.h>.

Un sémaphore est représenté par une variable sem_t. Avant de l’utiliser, vous devez l’initialiser par le biais de la fonction sem_init, à laquelle vous passez un pointeur sur la variable sem_t. Le second paramètre doit être à zéro3), et le troisième paramètre est la valeur initiale du sémaphore. Si vous n’avez plus besoin d’un sémaphore, il est conseillé de libérer les ressources qu’il occupe avec sem_destroy.

Pour vous mettre en attente sur un sémaphore, utilisez sem_wait. Pour effectuer une opération de réveil, utilisez sem_post. Une fonction permettant une mise en attente non bloquante, sem_trywait, est également fournie. Elle est similaire à pthread_mutex_trylock − si l’attente devait bloquer en raison d’un sémaphore à zéro, la fonction se termine immédiatement, avec le code d’erreur EAGAIN, au lieu de bloquer le thread.

GNU/Linux fournit également une fonction permettant d’obtenir la valeur courante d’un sémaphore, sem_getvalue, qui place la valeur dans la variable int pointée par son second argument. Vous ne devriez cependant pas utiliser la valeur du sémaphore que vous obtenez à partir de cette fonction pour décider d’une opération d’attente ou de réveil sur un sémaphore. Utiliser ce genre de méthode peut conduire à des conditions de concurrence critique: un autre thread peut changer la valeur du sémaphore entre l’appel de sem_getvalue et l’appel à une autre fonction de manipulation des sémaphores. Utilisez plutôt les fonctions d’opérations d’attente et de réveil atomiques.

Pour revenir à notre exemple de file d’attente de tâches, nous pouvons utiliser un sémaphore pour compter le nombre de tâches en attente dans la file. Le Listing !!jobqueue3!! contrôle la file avec un sémaphore. La fonction enqueue_job ajoute une nouvelle tâche à la file d’attente.

Listing (job-queue3.c) File de Tâches Contrôlée par un Sémaphore

#include <malloc.h>
#include <pthread.h>
#include <semaphore.h>
struct job {
   /* Champ de chaînage.  */
   struct job* next;
   /* Autres champs décrivant la tâche... */
};
/* Liste chaînée des tâches en attente. */
struct job* job_queue;
/* Mutex protégeant job_queue. */
pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Sémaphore comptant le nombre de tâches dans la file. */
sem_t job_queue_count;
/* Initialisation de la file de tâches. */
void initialize_job_queue ()
{
  /* La file est initialement vide. */
  job_queue = NULL;
  /* Initialise le sémaphore avec le nombre de tâches dans la file. Sa valeur
     initiale est zéro. */
  sem_init (&job_queue_count, 0, 0);
}
/* Traite les tâches en attente jusqu'à ce que la file soit vide. */
void* thread_function (void* arg)
{
  while (1) {
    struct job* next_job;
    /* Se met en attente sur le sémaphore de la file de tâches. Si sa valeur
       est positive, indiquant que la file n'est pas vide, décrémente
       le total d'une unité. Si la file est vide, bloque jusqu'à ce
       qu'une nouvelle tâche soit mise en attente. */
    sem_wait (&job_queue_count);
    /* Verrouille le mutex sur la file de tâches. */
    pthread_mutex_lock (&job_queue_mutex);
    /* À cause du sémaphore, nous savons que la file n'est pas vide. Récupère
    donc la prochaine tâche disponible. */
    next_job = job_queue;
    /* Supprime la tâche de la liste. */
    job_queue = job_queue->next;
    /* Déverrouille le mutex de la file d'attente car nous en avons fini avec
       celle-ci pour le moment. */
    pthread_mutex_unlock (&job_queue_mutex);
    /* Traite la tâche. */
    process_job (next_job);
    /* Libération des ressources. */
    free (next_job);
  }
  return NULL;
}
/* Ajoute une nouvelle tâche en tête de la file.  */
void enqueue_job (/* Passez les données sur la tâche ici */)
{
  struct job* new_job;
  /* Alloue un nouvel objet job. */
  new_job = (struct job*) malloc (sizeof (struct job));
  /* Positionner les autres champs de la struct job ici... */
  /* Verrouille le mutex de la file de tâches avant d'y accéder. */
  pthread_mutex_lock (&job_queue_mutex);
  /* Ajoute la nouvelle tâche en tête de file. */
  new_job->next = job_queue;
  job_queue = new_job;
  /* Envoie un signal de réveil sémaphore pour indiquer qu'une nouvelle tâche 
     est disponible. Si des threads sont bloqués en attente sur le sémaphore,
     l'un d'eux sera débloqué et pourra donc traiter la tâche. */
  sem_post (&job_queue_count);
  /* Déverrouille le mutex de la file de tâches. */
  pthread_mutex_unlock (&job_queue_mutex);
}

Avant de prélever une tâche en tête de file, chaque thread se mettra en attente sur le sémaphore. Si la valeur de celui-ci est zéro, indiquant que la file est vide, le thread sera tout simplement bloqué jusqu’à ce que le sémaphore devienne positif, indiquant que la tâche a été ajoutée à la file.

La fonction enqueue_job ajoute une tâche à la file. Comme thread_function, elle doit verrouiller le mutex de la file avant de la modifier. Après avoir ajouté la tâche à la file, elle envoie un signal de réveil au sémaphore, indiquant qu’une nouvelle tâche est disponible. Dans la version du Listing !!jobqueue3!!, les threads qui traitent les tâches ne se terminent jamais; si aucune tâche n’est disponible pendant un certain temps, ils sont simplement bloqués dans sem_wait.

Variables de condition

Nous avons montré comment utiliser un mutex pour protéger une variable contre les accès simultanés de deux threads et comment utiliser les sémaphores pour implémenter un compteur partagé. Une variable de condition est un troisième dispositif de synchronisation que fournit GNU/Linux ; avec ce genre de mécanisme, vous pouvez implémenter des conditions d’exécution plus complexes pour le thread.

Supposons que vous écriviez une fonction de thread qui exécute une boucle infinie, accomplissant une tâche à chaque itération. La boucle du thread, cependant, a besoin d’être contrôlée par un indicateur: la boucle ne s’exécute que lorsqu’il est actif; dans le cas contraire, la boucle est mise en pause.

Le Listing !!spincondvar!! montre comment vous pourriez l’implémenter au moyen d’une simple boucle. À chaque itération, la fonction de thread vérifie que l’indicateur est actif. Comme plusieurs threads accèdent à l’indicateur, il est protégé par un mutex. Cette implémentation peut être correcte mais n’est pas efficace. La fonction de thread utilisera du temps processeur, que l’indicateur soit actif ou non, à vérifier et revérifier cet indicateur, verrouillant et déverrouillant le mutex à chaque fois. Ce dont vous avez réellement besoin, est un moyen de mettre le thread en pause lorsque l’indicateur n’est pas actif, jusqu’à ce qu’un certain changement survienne qui pourrait provoquer l’activation de l’indicateur.

Listing (spin-condvar.c) Implémentation Simple de Variable de Condition

#include <pthread.h>
int thread_flag;
pthread_mutex_t thread_flag_mutex;
void initialize_flag ()
{
  pthread_mutex_init (&thread_flag_mutex, NULL);
  thread_flag = 0;
}
/* Appelle do_work de façon répétée tant que l'indicateur est actif ; sinon,
   tourne dans la boucle. */
void* thread_function (void* thread_arg)
{
  while (1) {
    int flag_is_set;
    /* Protège l'indicateur avec un mutex. */
    pthread_mutex_lock (&thread_flag_mutex);
    flag_is_set = thread_flag;
    pthread_mutex_unlock (&thread_flag_mutex);
    if (flag_is_set)
      do_work ();
    /* Rien à faire sinon, à part boucler.  */
  }
  return NULL;
}
/* Positionne la valeur de l'indicateur de thread à FLAG_VALUE.  */
void set_thread_flag (int flag_value)
{
  /* Protège l'indicateur avec un verrouillage de mutex. */
  pthread_mutex_lock (&thread_flag_mutex);
  thread_flag = flag_value;
  pthread_mutex_unlock (&thread_flag_mutex);
}

Une variable de condition vous permet de spécifier une condition qui lorsqu’elle est remplie autorise l’exécution du thread et inversement, une condition qui lorsqu’elle est remplie bloque le thread. Du moment que tous les threads susceptibles de modifier la condition utilisent la variable de condition correctement, Linux garantit que les threads bloqués à cause de la condition seront débloqués lorsque la condition change.

Comme pour les sémaphores, un thread peut se mettre en attente sur une variable de condition. Si le thread A est en attente sur une variable de condition, il est bloqué jusqu’à ce qu’un autre thread, le thread B, valide la même variable de condition. Contrairement à un sémaphore, une variable de condition ne dispose pas de compteur ou de mémoire; le thread A doit se mettre en attente sur la variable de condition avant que le thread B ne la valide. Si le thread B valide la condition avant que le thread A ne fasse un wait dessus, la validation est perdue, et le thread A reste bloqué jusqu’à ce qu’un autre thread ne valide la variable de condition à son tour.

Voici comment vous utiliseriez une variable de condition pour rendre l’exemple précédent plus efficace:

  • La boucle dans thread_function vérifie l’indicateur. S’il n’est pas actif, le thread se met en attente sur la variable de condition.
  • La fonction set_thread_flag valide la variable de condition après avoir changé la valeur de l’indicateur. De cette façon, si thread_function est bloquée sur la variable de condition, elle sera débloquée et vérifiera la condition à nouveau.

Il y a un problème avec cette façon de faire: il y a une concurrence critique entre la vérification de la valeur de l’indicateur et la validation ou l’attente sur la variable de condition. Supposons que thread_function ait vérifié l’indicateur et ait déterminé qu’il n’était pas actif. À ce moment, l’ordonnancer de Linux suspend ce thread et relance le principal. Par hasard, le thread principal est dans set_thread_flag. Il active l’indicateur puis valide la variable de condition. Comme aucun thread n’est en attente sur la variable de condition à ce moment (souvenez-vous que thread_function a été suspendue avant de se mettre en attente sur la variable de condition), la validation est perdue. Maintenant, lorsque Linux relance l’autre thread, il commence à attendre la variable de condition et peut être bloqué pour toujours.

Pour résoudre ce problème, nous avons besoin de verrouiller l’indicateur et la variable de condition avec un seul mutex. Heureusement, GNU/Linux dispose exactement de ce mécanisme. Chaque variable de condition doit être utilisée en conjugaison avec un mutex, pour éviter ce type de concurrence critique. Avec ce principe, la fonction de thread suit les étapes suivantes:

  • La boucle dans thread_function verrouille le mutex et lit la valeur de l’indicateur.
  • Si l’indicateur est actif, elle déverrouille le mutex et exécute la fonction de traitement.
  • Si l’indicateur n’est pas actif, elle déverrouille le mutex de façon atomique et se met en attente sur la variable de condition.

La fonctionnalité critique utilisée se trouve dans l’étape 3, GNU/Linux vous permet de déverrouiller le mutex et de vous mettre en attente sur la variable de condition de façon atomique, sans qu’un autre thread puisse intervenir. Cela élimine le risque qu’un autre thread ne change la valeur de l’indicateur et ne valide la variable de condition entre le test de l’indicateur et la mise en attente sur la variable de condition de thread_function.

Une variable de condition est représentée par une instance de pthread_cond_t. Souvenez-vous que chaque variable de condition doit être accompagnée d’un mutex. Voici les fonctions qui manipulent les variables de condition:

  • pthread_cond_init initialise une variable de condition. Le premier argument est un pointeur vers une variable pthead_cond_t. Le second argument, un pointeur vers un objet d’attributs de variable de condition, est ignoré par GNU/Linux. Le mutex doit être initialisé à part, comme indiqué dans la Section 4.4.2, « Mutexes ».
  • pthread_cond_signal valide une variable de condition. Un seul des threads bloqués sur la variable de condition est débloqué. Si aucun thread n’est bloqué sur la variable de condition, le signal est ignoré. L’argument est un pointeur vers la variable pthread_cond_t.
    Un appel similaire, pthread_cond_broadcast, débloque tous les threads bloqués sur une variable de condition, au lieu d’un seul
  • pthread_cond_wait bloque l’appelant jusqu’à ce que la variable de condition soit validée. L’argument est un pointeur vers la variable pthread_cond_t. Le second argument est un pointeur vers la variable pthread_mutex_t.
    Lorsque pthread_cond_wait est appelée, le mutex doit déjà être verrouillé par le thread appelant. Cette fonction déverrouille automatiquement le mutex et se met en attente sur la variable de condition. Lorsque la variable de condition est validée et que le thread appelant est débloqué, pthread_cond_wait réacquiert automatiquement un verrou sur le mutex.

Lorsque votre programme effectue une action qui pourrait modifier la condition que vous protégez avec la variable de condition, il doit suivre les étapes suivante (dans notre exemple, la condition est l’état de l’indicateur du thread, donc ces étapes doivent être suivies à chaque fois que l’indicateur est modifié):

  • Verrouiller le mutex accompagnant la variable de condition.
  • Effectuer l’action qui pourrait modifier la condition (dans notre exemple, activer l’indicateur).
  • Valider ou effectuer un broadcast sur la variable de condition, selon le comportement désiré.
  • Déverrouiller le mutex accompagnant la variable de condition.

Le Listing !!condvar!! reprend l’exemple précédent, en utilisant une variable de condition pour protéger l’indicateur. Notez qu’au sein de thread_function, un verrou est posé sur le mutex avant de vérifier la valeur de thread_flag. Ce verrou est automatiquement libéré par pthread_cond_wait avant qu’il ne se bloque et automatiquement réacquis ensuite. Notez également que set_thread_flag verrouille le mutex avant de définir la valeur de thread_flag et de valider le mutex.

Listing (condvar.c) Contrôler un Thread avec une Variable de Condition

#include <pthread.h>
int thread_flag;
pthread_cond_t thread_flag_cv;
pthread_mutex_t thread_flag_mutex;
void initialize_flag ()
{
  /* Initialise le mutex et la variable de condition. */
  pthread_mutex_init (&thread_flag_mutex, NULL);
  pthread_cond_init (&thread_flag_cv, NULL);
  /* Initialise la valeur de l'indicateur. */
  thread_flag = 0;
}
/* Appelle do_work de façon répétée tant que l'indicateur est actif ; bloque
   si l'indicateur n'est pas actif. */
void* thread_function (void* thread_arg)
{
  /* Boucle infinie. */
  while (1) {
    /* Verrouille le mutex avant d'accéder à la valeur de l'indicateur. */
    pthread_mutex_lock (&thread_flag_mutex);
    while (!thread_flag)
      /* L'indicateur est inactif. Attend la validation de la variable de 
         condition, indiquant que la valeur de l'indicateur a changé. Lorsque 
         la validation a lieu et que le thread se débloque, boucle et teste à 
         nouveau l'indicateur. */
      pthread_cond_wait (&thread_flag_cv, &thread_flag_mutex);
    /* Lorsque nous arrivons ici, nous savons que l'indicateur est actif.
       Déverrouille le mutex. */
    pthread_mutex_unlock (&thread_flag_mutex);
    /* Actions utiles. */
    do_work ();
  }
  return NULL;
}
/* Définit la valeur de l'indicateur à FLAG_VALUE.  */
void set_thread_flag (int flag_value)
{
  /* Verrouille le mutex avant d'accéder à la valeur de l'indicateur. */
  pthread_mutex_lock (&thread_flag_mutex);
  /* Définit la valeur de l'indicateur, puis valide la condition, si jamais
     thread_function est bloquée en attente de l'activation de l'indicateur.
     Cependant, thread_function ne peut pas réellement tester la valeur
     de l'indicateur tant que le mutex n'est pas déverrouillé. */
  thread_flag = flag_value;
  pthread_cond_signal (&thread_flag_cv);
  /* Déverrouille le mutex. */
  pthread_mutex_unlock (&thread_flag_mutex);
}

La condition protégée par une variable de condition peut être d’une complexité quelconque. Cependant, avant d’effectuer une opération qui pourrait modifier la condition, un verrouillage du mutex doit être demandé, après quoi la variable de condition doit être validée.

Une variable de condition peut aussi être utilisée sans condition, simplement pour bloquer un thread jusqu’à ce qu’un autre thread le « réveille ». Un sémaphore peut également être utilisé pour ce faire. La principale différence est qu’un sémaphore se « souvient » de l’appel de réveil, même si aucun thread n’était bloqué en attente à ce moment, alors qu’une variable de condition ignore les appels de réveil, à moins qu’un thread ne soit bloqué en attente à ce moment. Qui plus est, un sémaphore ne réactive qu’un thread par signal de réveil; avec pthread_cond_broadcast, un nombre quelconque et inconnu de threads bloqués peuvent être relancés en une fois.

Interblocage avec deux threads ou plus

Des interblocages peuvent survenir lorsque deux threads (ou plus) sont bloqués, chacun attendant une validation de condition que seul l’autre peut effectuer. Par exemple, si le thread A est bloqué sur une variable de condition attendant que le thread B la valide et le thread B est bloqué sur une variable de condition attendant que le thread A la valide, un interblocage survient car aucun thread ne pourra jamais valider la variable attendue par l’autre. Vous devez être attentif afin d’éviter l’apparition de telles situations car elles sont assez difficiles à détecter.

Une situation courante menant à un interblocage survient lorsque plusieurs threads tentent de verrouiller le même ensemble d’objets. Par exemple, considérons un programme dans lequel deux threads différents, exécutant deux fonctions de thread distinctes, ont besoin de verrouiller les deux mêmes mutexes. Supposons que le thread A verrouille le mutex 1, puis le mutex 2 et que le thread B verrouille le mutex 2 avant le mutex 1. Avec un scénario d’ordonnancement pessimiste, Linux pourrait donner la main au thread A suffisamment longtemps pour qu’il verrouille le mutex 1 puis donne la main au thread B qui verrouille immédiatement le mutex 2. Désormais, aucun thread ne peut plus avancer cas chacun est bloqué sur un mutex que l’autre maintient verrouillé.

C’est un exemple de problème d’interblocage général qui peut impliquer non seulement des objets de synchronisation, comme les mutex, mais également d’autres ressources, comme des verrous sur des fichiers ou des périphériques. Le problème survient lorsque plusieurs threads tentent de verrouiller le même ensemble de ressources dans des ordres différents. La solution est de s’assurer que tous les threads qui verrouillent plus d’une ressource le font dans le même ordre.

Implémentation des threads sous GNU/Linux

L’implémentation des threads POSIX sous GNU/Linux diffère de l’implémentation des threads sous beaucoup de systèmes de type UNIX sur un point important: sous GNU/Linux, les threads sont implémentés comme des processus. Lorsque vous appelez pthread_create pour créer un nouveau thread, Linux crée un nouveau processus qui exécute ce thread. Cependant, ce processus n’est pas identique à ceux créés au moyen de fork; en particulier, il partage son espace d’adressage et ses ressources avec le processus original au lieu d’en recevoir des copies.

Le programme thread-pid du Listing !!threadpid!! le démontre. Le programme crée un thread; le thread original et le nouveau appellent tous deux la fonction getpid et affichent leurs identifiants de processus respectifs, puis bouclent indéfiniment.

Listing (thread-pid.c) Affiche les Identifiants de Processus des Threads

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function (void* arg)
{
  fprintf (stderr, "L'identifiant du thread fils est %d\n", (int) getpid ());
  /* Boucle indéfiniment. */
  while (1);
  return NULL;
}
int main ()
{
  pthread_t thread;
  fprintf(stderr, "L'identifiant du thread principal est %d\n",(int)getpid());
  pthread_create (&thread, NULL, &thread_function, NULL);
  /* Boucle indéfiniment. */
  while (1);
  return 0;
}

Lancez le programme en arrière-plan puis invoquez ps x pour afficher vos processus en cours d’exécution. N’oubliez pas de tuer le programme thread-pid ensuite − il consomme beaucoup de temps processeur pour rien. Voici à quoi la sortie pourrait ressembler:

% cc thread-pid.c -o thread-pid -lpthread
% ./thread-pid &
[1] 14608
main thread pid is 14608
child thread pid is 14610
% ps x
  PID TTY      STAT   TIME COMMAND
14042 pts/9    S      0:00 bash
14608 pts/9    R      0:01 ./thread-pid
14609 pts/9   S 0:00 ./thread-pid
14610 pts/9   R 0:01 ./thread-pid
14611 pts/9   R 0:00 ps x
% kill 14608
[1]+ Terminated         ./thread-pid
Notifications de contrôle des tâches dans le shell
Les lignes débutant par [1] viennent du shell. Lorsque vous exécutez un programme en arrière-plan, le shell lui assigne un numéro de tâche − dans ce cas, 1 − et affiche l’identifiant de processus du programme. Si une tâche en arrière-plan se termine, le shell le signale lorsque vous invoquez une nouvelle commande.

Remarquez qu’il y a trois processus exécutant le programme thread-pid. Le premier, avec le pid 14608, est le thread principal du programme ; le troisième, avec le pid 14610, est le thread que nous avons créé pour exécuter thread_function.

Qu’en est-il du second thread, avec le pid 14609 ? Il s’agit du « thread de gestion » (manager thread) qui fait partie de l’implémentation interne des threads sous GNU/Linux. Le thread de contrôle est créé la première fois qu’un programme appelle pthread_create pour créer un nouveau thread.

Gestion de signaux

Supposons qu’un programme multithreadé reçoive un signal. Dans quel thread est invoqué le gestionnaire de signal? Le comportement de l’interaction entre les threads et les signaux varie d’un type d’UNIX à l’autre. Sous GNU/Linux, ce comportement est dicté par le fait que les threads sont implémentés comme des processus.

Comme chaque thread est un processus distinct, et comme un signal est délivré à un processus particulier, il n’y a pas d’ambiguïté au niveau du thread qui va recevoir le signal. Typiquement, les signaux envoyés de l’extérieur du programme sont envoyés au processus correspondant au thread principal du programme. Par exemple, si un processus se divise et que le processus fils exécute un programme multithreadé, le processus père conservera l’identifiant de processus du thread principal du programme du processus fils et l’utilisera pour envoyer des signaux à son fils. Il s’agit généralement d’un bonne convention que vous devriez suivre lorsque vous envoyez des signaux à un programme multithreadé.

Notez que cet aspect de l’implémentation de pthreads sous GNU/Linux va à l’encontre du standard de threads POSIX. Ne vous reposez pas sur ce comportement au sein de programmes destinés à être portables.

Au sein d’un programme multithreadé, il est possible pour un thread d’envoyer un signal à un autre thread bien défini. Utilisez la fonction pthread_kill pour cela. Son premier paramètre est l’identifiant du thread et le second, le numéro du signal.

L'appel système clone

Bien que les threads GNU/Linux créés dans le même programme soient implémentés comme des processus séparés, ils partagent leur espace mémoire virtuel et leurs autres ressources. Un processus fils créé avec fork, cependant, copie ces objets. Comment est créé le premier type de processus?

L’appel système clone de Linux est une forme hybride entre fork et pthread_create qui permet à l’appelant de spécifier les ressources partagées entre lui et le nouveau processus. clone nécessite également que vous spécifiiez la région mémoire utilisée pour la pile d’exécution du nouveau processus. Bien que nous mentionnions clone pour satisfaire la curiosité du lecteur, cet appel système ne doit pas être utilisé au sein de programmes. Utilisez fork pour créer de nouveau processus ou pthread_create pour créer des threads.

Comparaison processus/threads

Pour certains programmes tirant partie du parallélisme, le choix entre processus et threads peut être difficile. Voici quelques pistes pour vous aider à déterminer le modèle de parallélisme qui convient le mieux à votre programme:

  • Tous les threads d’un programme doivent exécuter le même code. Un processus fils, au contraire, peut exécuter un programme différent en utilisant une fonction exec.
  • Un thread peut endommager les données d’autres threads du même processus car les threads partagent le même espace mémoire et leurs ressources. Par exemple, une écriture sauvage en mémoire via un pointeur non initialisé au sein d’un thread peut corrompre la mémoire d’un autre thread.
    Un processus corrompu par contre, ne peut pas agir de cette façon car chaque processus dispose de sa propre copie de l’espace mémoire du programme.
  • Copier le contenu de la mémoire pour un nouveau processus a un coût en performances par rapport à la création d’un nouveau thread. Cependant, la copie n’est effectuée que lorsque la mémoire est modifiée, donc ce coût est minime si le processus fils ne fait que lire la mémoire.
  • Les threads devraient être utilisés pour les programmes qui ont besoin d’un parallélisme finement contrôlé. Par exemple, si un problème peut être décomposé en plusieurs tâches presque identiques, les threads peuvent être un bon choix. Les processus devraient être utilisés pour des programmes ayant besoin d’un parallélisme plus grossier.
  • Le partage de données entre des threads est trivial car ceux-ci partagent le même espace mémoire (cependant, il faut faire très attention à éviter les conditions de concurrence critique, comme expliqué plus haut). Le partage de données entre des processus nécessite l’utilisation de mécanismes IPC, comme expliqué dans le Chapitre 5. Cela peut être plus complexe mais diminue les risques que les processus souffrent de bugs liés au parallélisme.
1) NdT. Appelés aussi « processus légers ».
2) Notez que cette façon de faire n’est pas portable et qu’il est de votre responsabilité de vous assurer que la valeur peut être convertie en toute sécurité vers et depuis void* sans perte de bit.
3) Une valeur différente de zéro indiquerait que le sémaphore peut être partagé entre les processus, ce qui n’est pas supporté sous GNU/Linux pour ce type de sémaphore.
 
livre/chap4/threads.txt · Dernière modification: 2007/09/01 18:51 par david
 
Recent changes RSS feed Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki Hosted by TuxFamily