• Aucun résultat trouvé

Création de threads

Dans le document Programmation système en C sous (Page 148-151)

Il existe grosso modo des équivalents aux appels-système fork( ), wait( ) et exit( ), qui permettent dans un contexte multithread de créer un nouveau thread, d'attendre la fin de son exécution, et de mettre fin au thread en cours.

Un type opaque pthread_t est utilisé pour distinguer les différents threads d'une application, à la manière des PID qui permettent d'identifier les processus. Dans la bibliothèque LinuxThreads, le type pthread_t est implémenté sous forme d'un unsigned long, mais sur d'autres systèmes il peut s'agir d'un type structuré. On se disciplinera donc pour employer systématiquement la fonction pthread_equal( ) lorsqu'on voudra comparer deux identifiants de threads.

int pthread_equal (pthread_t thread_1, pthread_t thread_2);

Cette fonction renvoie une valeur non nulle s'ils sont égaux.

Lors de la création d'un nouveau thread, on emploie la fonction pthread_create( ).

Celle-ci donne naissance à un nouveau fil d'exécution, qui va démarrer en invoquant la routine dont on passe le nom en argument. Lorsque cette routine se termine, le thread est éliminé. Cette routine fonctionne donc un peu comme la fonction main( ) des programmes C. Pour cette raison, le fil d'exécution original du processus est nommé thread principal (main thread). Le prototype de pthread_create( ) est le suivant : int pthread_create (pthread_t * thread, pthread_attr_t * attributs, void * (* fonction) (void * argument),

void * argument);

Le premier argument est un pointeur qui sera initialisé par la routine avec l'identifiant du nouveau thread. Le second argument correspond aux attributs dont on désire doter le nouveau thread. Ces attributs seront détaillés dans la prochaine section. En général, on transmettra en second argument un pointeur NULL, car le thread reçoit alors les attributs standard par défaut.

Le troisième argument est un pointeur représentant la fonction principale du nouveau thread. Celle-ci est invoquée dès la création du thread et reçoit en argument le pointeur passé en dernière position dans pthread_create( ). Le type de l'argument étant void *, on pourra le transformer en n'importe quel type de pointeur pour passer un argument au thread. On pourra même employer une conversion explicite en int pour transmettre une valeur. Cet argument est généralement un numéro permettant au thread de déterminer le travail qu'il doit accomplir — en employant la même fonction principale pour plusieurs threads —, mais cela peut aussi être un pointeur sur une structure que le nouveau thread doit manipuler avant de se terminer. Cette routine renvoie zéro si elle réussit et une valeur non nulle sinon, correspondant à l'erreur survenue. En effet, comme l'essentiel des fonctions de la bibliothèque Pthreads, pthread_create( ) ne remplit pas nécessairement la variable globale errno1. Le nombre de threads simultanés est limité par la constante PTHREAD_THREADS_MAX (1 024 avec LinuxThreads).

Lorsque la fonction principale d'un thread se termine, celui-ci est éliminé. Cette fonction doit renvoyer une valeur de type void * qui pourra être récupérée dans un autre fil d'exécution. Il est aussi possible d'invoquer la fonction pthread_exit( ), qui met fin au thread appelant tout en renvoyant le pointeur void * passé en argument. On ne doit naturellement pas invoquer exit( ), qui mettrait fin à toute l'application et pas uniquement au thread appelant.

void pthread_exit (void * retour);

Lorsque cette routine est appelée, toutes les fonctions de nettoyage final que nous observerons ultérieurement sont invoquées dans l'ordre inverse de leur enregistrement.

Pour récupérer la valeur de retour d'un thread terminé, on utilise la fonction pthread_join( ). Celle-ci suspend l'exécution du thread appelant jusqu'à la terminaison du thread indiqué en argument. Elle remplit alors le pointeur passé en seconde position avec la valeur de retour du thread fini.

int pthread_join (pthread_t thread, void ** retour);

Lorsqu'on désire employer une valeur de type entier comme code de retour, il est important de procéder en deux étapes pour la récupérer depuis le type void *. Ceci permet de conserver la portabilité du programme, même si la taille d'un void * est plus grande que celle d'un int. Nous rencontrerons cette situation dans les programmes d'exemples décrits ci-dessous. Dans certaines conditions, un thread peut être annulé par un autre thread, un peu à la manière d'un processus qui peut être tué par un signal.

Dans ce cas, le thread terminé n'a pu renvoyer de valeur, aussi la variable * retour prend-elle une valeur particulière : PTHREAD_CANCEL. La norme Posix nous garantit que cette constante ne peut pas correspondre à une adresse valide, pas plus qu'à la valeur NULL d'ailleurs. La fonction pthread_join( ) peut échouer si le thread attendu n'existe pas, s'il est détaché, comme nous le verrons plus bas, ou si un risque de blocage se présente — par exemple si un thread demande à attendre sa propre fin.

Nous pouvons déjà employer ces fonctions pour écrire un petit programme simpliste avec plusieurs fils d'exécution incrémentant un compteur jusqu'à ce qu'il atteigne 40, puis qui se termine.

exemple_create.c :

#include <pthread.h>

#include <stdio.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <unistd.h>

#define NB THREADS 5

void * fn_thread (void * numero);

static int compteur = 0;

int main (void) {

pthread_t thread [NB_THREADS];

int i;

int ret;

for (i = 0; i < NB THREADS; i ++)

if ((ret = pthread_create (& thread [i], NULL,

fn_thread,

(void *) i)) != 0) { fprintf (stderr, "%s", strerror (ret));

exit (1);

}

while (compteur < 40) {

fprintf (stdout, "main : compteur = %d \n", compteur);

sleep (1);

}

for (i = 0; i < NB THREADS; i ++) pthread_join (thread [i], NULL);

return (0);

}

void *

fn_thread (void * num) {

int numero = (int) num;

while (compteur < 40) { usleep (numero * 100000);

compteur ++;

fprintf (stdout, "Thread %d : compteur = %d \n", numero, compteur);

}

pthread_exit (NULL);

}

Lors de son exécution, ce programme montre bien l'enchevêtrement des différents threads :

$ ./exemple_create main : compteur = 0 Thread 0 : compteur 1 Thread 0 : compteur 2 Thread 0 : compteur 3 Thread 0 : compteur 4 Thread 0 : compteur 5 Thread 0 : compteur 6 Thread 0 : compteur 7 Thread 0 : compteur 8 Thread 0 : compteur 9 Thread 0 : compteur 10 Thread 0 : compteur 11 Thread 1 : compteur 12 Thread 0 : compteur 13 Thread 0 : compteur 14 Thread 0 : compteur 15 Thread 0 : compteur 16 Thread 0 : compteur 17 Thread 0 : compteur 18 Thread 0 : compteur 19 Thread 0 : compteur 20 Thread 0 : compteur 21 Thread 0 : compteur 22 Thread 2 : compteur 23 Thread 0 : compteur 24 Thread 1 : compteur 25 Thread 0 : compteur 26 Thread 0 : compteur 27 Thread 0 : compteur 28 Thread 0 : compteur 29 Thread 0 : compteur 30 Thread 0 : compteur 31 Thread 0 : compteur 32 Thread 0 : compteur 33 Thread 0 : compteur 34 Thread 3 : compteur 35 Thread 0 : compteur 36 Thread 0 : compteur 37 Thread 1 : compteur 38 Thread 0 : compteur 39 Thread 0 : compteur 40 Thread 4 : compteur 41 Thread 2 : compteur 42 Thread 1 : compteur 43 Thread 3 : compteur 44

$

Cette application est très mal conçue. car les différents threads modifient la même variable globale sans se préoccuper les uns des autres. Et c'est justement l'essence même de la programmation multithread d'éviter ce genre de situation, comme nous le verrons dans les prochains paragraphes.

Le programme suivant n'utilise qu'un seul thread autre que le fil d'exécution principal ; il s'agit simplement de vérifier le comportement des fonctions pthread_join( ) et pthread_exit( ). Nous sous-traitons la lecture d'une valeur au clavier dans un fil d'exécution secondaire. Le fil principal pourrait en profiter pour réaliser d'autres opérations.

exemple_join.c :

#include <pthread.h>

#include <stdio.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <unistd.h>

void *

fn_thread (void * inutile) {

char chaine [128];

int i = 0;

fprintf (stdout, "Thread : entrez un nombre :");

while (fgets (chaine, 128, stdin) != NULL) if (sscanf (chaine, "%d", & i) != 1) fprintf (stdout, "un nombre SVP :");

else

break;

pthread_exit ((void *) i);

} int main (void) {

int i;

int ret;

void * retour;

pthread_t thread;

if ((ret = pthread_create (& thread, NULL, fn_thread, NULL)) != 0) { fprintf (stderr, "%s\n", strerror (ret));

exit (1);

}

pthread_join (thread , & retour);

if (retour != PTHREAD_CANCELED) { i = (int) retour;

fprintf (stdout, "main : valeur lue = %d\n", i);

}

return (0);

}

Sans surprise, l'exécution se déroule ainsi :

$ ./exemple_join

Thread : entrez un nombre :4767 main : valeur lue = 4767

$

On notera la méthode correcte employée pour récupérer la valeur de retour dans l'appel pthread_join( ), en utilisant d'abord un pointeur void * temporaire, qu'on transforme ensuite en int.

Lorsqu'un thread ne renvoie pas de valeur intéressante et qu'il n'a pas besoin d'être attendu par un autre thread, on peut employer la fonction pthread_detach( ), qui lui permet de disparaître automatiquement du système quand il se termine. Cela autorise la libération immédiate des ressources privées du thread (pile et variables automatiques). Le mécanisme n'est pas sans rappeler le passage des processus à l'état Zombie en attendant qu'on lise leur code de retour et l'emploi de SIG_IGN pour le signal SIGCHLD du processus parent.

int pthread_detach (pthread_t thread);

Un thread peut très bien invoquer pthread_detach( ) à propos d'un autre thread de l'application. Contrairement aux processus, il n'y a pas de notion de hiérarchie chez les threads ni d'autorisations particulières pour modifier les paramètres d'un autre fil d'exécution. Cette fonction échoue si le thread n'existe pas ou s'il est déjà détaché.

Il n'est pas possible d'attendre, avec pthread_join( ), la fin d'un thread donné parmi tous ceux qui se déroulent. Ce genre de fonctionnalité pourrait être utile pour attendre que tous les threads d'un certain ensemble se terminent. On peut obtenir le même résultat en utilisant des threads détachés qui décrémentent un compteur global avant de se terminer, le thread principal surveillant alors l'état de ce compteur. Des précautions devront être prises pour l'accès au compteur global, comme nous le verrons plus tard.

Pour connaître son propre identifiant, un thread invoque la fonction pthread_self( ), qui lui renvoie une valeur de type pthread_t :

pthread_t pthread_self (void);

Il lui est alors possible de comparer. avec pthead_equal( ), son identité avec une variable globale indiquant une tâche à accomplir. Ainsi un programme peut utiliser ce genre de principe :

static pthread_t thr_dialogue_radar;

static pthread_t thr_dialogue_automate;

static pthread_t thr_dialogue_radar_2;

int main (void) {

pthread_create (& thr_dialogue_radar, NULL, dialogue, NULL);

pthread_create (& thr_dialogue_automate, NULL, dialogue, NULL);

pthread_create (& thr_dialogue_radar_2, NULL, dialogue, NULL);

[...]

}

void *

dialogue (void * inutile) {

initialisation_generale_dialogue( ) ;

if (pthread_equal (pthread_self( ), thr_dialogue_radar)) initialisation_specifique_radar( );

else if (pthread_equal (pthread_self( ), thr_dialogue_automate)) initialisation_specifique_automate( );

else if (pthread_equal (pthread_self( ), thr_dialogue_radar_2)) initialisation_specifique_radar2( );

[...]

}

Dans ce cas précis, on aurait avantageusement obtenu le même résultat en utilisant l'argument de la fonction dialogue( ). Toutefois, l'intérêt de la comparaison avec une variable globale pthread_t est de permettre d'adapter le comportement au sein de sous-routines profondément imbriquées, jusqu'auxquelles on ne désire pas faire descendre l'argument de dialogue( ).

Rappelons-nous que le type pthread_t est opaque et ne permet pas d'affectation directe.

Il n'est donc pas possible de mémoriser l'identifiant du thread principal, puisqu'il n'est pas créé par pthread_create( ). Si on en a besoin, il faut procéder en créant quand même un thread nouveau:

pthread_t thr_principal;

int main (void) {

void * retour;

pthread_create (& thr_principal, NULL, suite application, NULL);

pthread_join (thr_principal, & retour);

return ((int) retour);

}

void *

suite_application (void * inutile) {

[...]

pthread_exit ((void *) 0);

}

Dans le document Programmation système en C sous (Page 148-151)