Linux/Unix : Les threads

26 décembre 2013 rdorigny 0 commentaires

A la différence des processus, le thread ne dispose pas d'un espace mémoire dédié. Il doit mettre en place des mécanismes pour assurer les entrées/sorties au moment opportun. Nous étudierons ces mécanismes de verrouillage de la mémoire.

Pourquoi ce partage de l'espace mémoire? C'est un constat, dans le monde des processus on réserve beaucoup de mémoire qui est souvent peu utilisée, alors qu'il s'agit d'une ressource rare et coûteuse. Le thread dispose de l'ensemble de l'espace mémoire disponible, cela permet d'optimiser l'espace mémoire au plus juste. Mais cela implique que les différents threads doivent se gérer entre eux pour éviter les conflits d'écriture.



En POSIX, une application peut être composé de plusieurs processus, eux-même composés de plusieurs thread. La norme POSIX réalise des threds qui sont portables d'une plateforme à une autre, on parle des POSIX thread ou des P-thread.
Pour compiler un code qui traite des thread, il y a une syntaxe particulière:
cc -pthread -o pgr pgr.c

1)Les activités

On confond la dénomination activité et thread, c'est la même chose.

1.1)Les attributs d'une activité

Une activité possède un identifiant TID (Thread Identity) de type pthread_t qui est un entier. Ce numéro est complémentaire du PID pour identifier le processus.

La fonction getpid() retourne l'identifiant du processus UNIX:
#include <unistd.h> pid_t getpid();

La fonction pthread_self() retourne l'identité de l'activité en cours (thread actif):
#include <pthread.h> pthread_h pthread_self(void);

La fonction pthread_equal() permet de tester légalité entre deux identités tid1 et tid2, valeur (nulle si oui):
#include <pthread.h> int pthread_equal(pthread_ tid1,pthread_ tid2);
A noter qu'une simple comparaison est possible, mais pas conseillée car certaines architecture à base d'UNIX ont des tailles différentes pour l'objet pthread_t.

1.2)création/suppression d'une activité

Pour créer un thread, il suffit de faire un appel à la fonction pthread_create(). Elle retourne 0 en cas de succès et -1 sinon (la variable errno permet de connaitre l'erreur rencontrée). Si l'appel est réussi, *thread reçoit l'identité de l'activité. start_routine correspond à la fonction qui sera exécutée par l’activité et arg correspond au paramètre de la fonction start_routine. A noter que le paramètre attr définit les attributs de l'activité, avec la valeur NULL pour une utilisation par défaut. Sinon, on peut utiliser la fonction pthread_attr_init() pour initialiser la paramètre attr.
#include <pthread.h> int pthread_create(pthread_t *thread,ptread_attr_t * attr, void * (*start_routine) (void *),void *arg); int pthread_attr_init(pthread_attr_t *attr);


Voici un exemple de création de thread:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/types.h> #include <unistd.h> void *fct(){ printf("PID-TID du Thread fils:%d - %dn",getpid(),pthread_self()); } int main(){ pthread_t tid1,tid2; pthread_create(&tid1,NULL,fct,NULL); pthread_create(&tid2,NULL,fct,NULL); printf("PID-TID du Thread père:%d - %dn",getpid(),pthread_self()); exit(0); }


Ce qui donne:


Pour arrêter une activité, nous avons exit() et _exit(). Si le processus est arrêté, toutes les activités associées sont terminées. Pour arrêter une activité, il y aussi la fonction pthread_exit() qui termine l'activité appelante.
#include <pthread.h> void pthread_exit(void *retval);


Lorsqu'une activité se termine, elle ne disparaît pas totalement et les ressources en mémoires ne sont pas libérées, il faut pour cela faire un appel à la fonction pthread_join(). Elle attend la fin du thread pour libérer les ressources, il faut donc l'appeler depuis le processus père. Le paramètre th référence le thread à attendre et thread_return le code de retour éventuellement transmis par pthread_exit().
#include <pthread.h> int pthread_join(pthread_t th,void **thread_return);


On ajoute à l'exemple précédent la libération des ressources:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/types.h> #include <unistd.h> void *fct(){ printf("PID-TID du Thread fils:%d - %dn",getpid(),pthread_self()); } int main(){ pthread_t tid1,tid2; //Création des thread pthread_create(&tid1,NULL,fct,NULL); pthread_create(&tid2,NULL,fct,NULL); printf("PID-TID du Thread père:%d - %dn",getpid(),pthread_self()); //Attend la mort des thread pour continuer pthread_join(tid1,NULL); pthread_join(tid2,NULL); exit(0); }

1.3)Demande de libération des ressources

Nous avons vu la fonction pthread_join() qui libère les ressources lorsque l'activité se termine. Il existe d'autres fonctions comme pthread_detach() qui demande la libération des ressources au processus, auquel elle appartient, à la fin de vie de l'activité.

Il est impératif que les ressources soient libérées par un appel à la fonction pthread_join() ou pthread_detach().

La fonction pthread_cancel() demande l'abandon d'une autre activité, l'activité choisira si elle réalise l'abandon ou non avec les méthodes pthread_setcancelstate() ou pthread_setcanceltype().

#include <pthread.h> pthread_detach(pthread_t th); pthread_cancel(pthread_t tid); pthread_setcancelstate(int state,int *etat_pred); pthread_setcanceltype(int mode, int *ancien_mode);
Une pile d'appels de fonctions est associée à chaque activité qui sont réalisé lors de la terminaison, les fonctions pthread_cleanup_push() et pthread_cleanup_pop() permettent de retirer de la pile les fonctions en attente et de libérer les ressources plus rapidement.

#include <pthread.h> pthread_cleanup_push(void (*fonction)(void *arg),void *arg); pthread_cleanup_pop(int exe);

2)Les mutex

Un mutex permet de verrouiller une zone critique pour assurer l'unicité du travail d'une activité. C'est un mécanisme trés puissant que nous allons étudier ci-dessous.

2.1)création d'un mutex


#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr);

Avant l'appel, le mutex pointe sur la zone réservée du futur mutex, mutteattr est l'ensemble des attributs à affecter au mutex, soit NULL par défaut. Après l'appel, le nouveau mutex pointe sur la zone dédiée. La fonction retourne 0 en cas de succès.

La primitive P pour un mutex:
L'appel bloquant pthread_mutex_lock() réserve un mutex ou attend que le mutex se libère pour le réserver au profit du thread appelant. La fonction retourne 0 en cas de succès.
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex);

Il existe un appel non bloquant (test et passe à la suite, pas d'attente contrairement à la fonction précédente) qui est assuré par la fonction pthread_mutex_trylock(). 0 en cas de succès (errno=EBUSY si occupé et errno=EINVAL si le mutex n'est pas initialisé
#include <pthread.h> int pthread_mutex_trylock(pthread_mutex_t *mutex);

La primitive V pour un mutex:
La fonction pthread_mutex_unlock() permet de débloquer une activité en attente sur ce mutex. Elle retourne 0 en cas de succès.
#include <pthread.h> int pthread_mutex_unlock(pthread_mutex_t *mutex);

2.2)Destruction d'un mutex

Pour détruire un mutex, il y a la fonction pthread_mutex_destroy().
#include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex);

2.3)Exemple d'utilisation

Voici un exemple d'utilisation de l'utilisation d'un mutex. On créé trois fonctions qui attendent la fin de la précédente pour se lancer.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/types.h> #include <unistd.h> int global=0; pthread_mutex_t mymutex; void *fct(){ printf("PID-TID du Thread fils:%d - %dn",getpid(),pthread_self()); pthread_mutex_lock(&mymutex); global++; printf("Section critique num:%dn",global); sleep(10); printf("Fin de section critiquen"); pthread_mutex_unlock(&mymutex); pthread_exit(NULL); } int main(){ pthread_t tid[3]; int i; //Initialisation du mutex pthread_mutex_init(&mymutex,NULL); //Création des threads for (i=0;i<3;i++) pthread_create(tid+i,NULL,fct,NULL); //Attente fin des thread for (i=0;i<3;i++) pthread_join(tid[i],NULL); pthread_mutex_destroy(&mymutex); exit(0); }

Ce qui donne:

3)Les conditions autour du mutex

La condition des mutex permet de synchroniser plusieurs activités dans une section critique. Le mécanisme générale :
Verrouillage par le mutex Tant que la ressource est indisponible, attente de la condition Récupération de la ressource Déverrouillage du mutex

3.1)Initialisation d'une condition

Il faut préparer la condition en appelant la fonction pthread_cond_init(), cond est un pointeur sur l'espace mémoire réservé à la condition et cond_attr précise les attributs de la condition. Après l'appel, cond pointera sur la nouvelle condition. La fonction retourne 0 en cas de succès.
#include <pthread.h> int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

3.2)Attente sur une condition

La fonction pthread_cond_wait a un fonctionnement assez particulier:
  • 1: Débloque le mutex associée à la condition,
  • 2: Attente jusqu'à la condition,
  • 3: Fin d'attente, bloque le mutex et retour au programme.
  • #include <pthread.h> int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

    Le problème de la fonction précédente est que l'attente n'est pas bornée, elle peut être infinie! Aussi, il existe une fonction qui attend un temps limité. On lui passe un objet abstime pour définir le temps à patienter.
    #include <pthread.h> int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime); struct timespec{ unsigned log tv_sec //secondes long tv_nsec; //nanosecondes };

    3.3)Envoi d'un signal

    Cela permet d'activer un thread en attente sur une condition.
    #include <pthread.h> int pthread_cond_signal(pthread_cond_t *cond);

    Il est également possible d'envoyer un signal à toutes les activités:
    #include <pthread.h> int pthread_cond_broadcast(pthread_cond_t *cond);

    3.4)Suppression d'une condition

    #include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond);

    3.5)Exemple

    Voici un exemple de code ou quatre thread doivent se partager 2 outils pour travailler, et donc il y a des threads en attente.

    #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/types.h> #include <unistd.h> #define NBTHREAD 4 int outils=2; pthread_mutex_t mymutex; pthread_cond_t mycond; void *fct(){ printf("PID-TID du Thread fils:%d - %dn",getpid(),pthread_self()); sleep(1); pthread_mutex_lock(&mymutex); while (outils==0) pthread_cond_wait(&mycond,&mymutex); outils--; printf("Debut travail outils:%dn",outils); pthread_mutex_unlock(&mymutex); sleep(10); pthread_mutex_lock(&mymutex); outils++; printf("Fin travail outils:%dn",outils); pthread_mutex_unlock(&mymutex); pthread_cond_signal(&mycond); pthread_exit(NULL); } int main(){ pthread_t tid[NBTHREAD]; int i; //Initialisation pthread_mutex_init(&mymutex,NULL); pthread_cond_init(&mycond,NULL); //Création des threads for (i=0;i<NBTHREAD;i++) pthread_create(tid+i,NULL,fct,NULL); //Attente fin des thread for (i=0;i<NBTHREAD;i++) pthread_join(tid[i],NULL); pthread_mutex_destroy(&mymutex); pthread_cond_destroy(&mycond); exit(0); }

    Ce qui donne:

    4) Autres fonctions relatives

    4.1) Demande explicite de libération

    Le système Unix ou Linux dispose d'un ordonnanceur, qui permet de gérer les activités multi-threading. Il s'agit d'un mécanisme autonome, mais fort heureusement il existe un fonction pour agir dessus. Cela se fait par la fonction sched_yield(), ainsi en l'utilisant, un thread peut demander de rendre la main (fonctionne également avec les processus).

    #include <sched.h> void sched_yield();

    4.2) Appel unique à une fonction

    Il existe un mécanisme qui permet de lancer une fonction une seule fois, utile notamment dans un milieu multi-thread pour initialiser les variables. Il est nécessaire pour ce faire d'initialiser une variable pthread_once_t avec PTHREAD_ONCE_INIT puis on utilise la fonction pthread_once(). Voici un exemple:

    #include <pthread.h> static pthread_once_t myonce=PTHREAD_ONCE_INIT; static pthread_mutex_t mymutex; void * mutex_init(){ .... pthread_mutex_init(&mymutex,NULL); .... } .... pthread_once(&myonce,mutex_init); ....

    5)Gestion des thread sous Linux

    Linux ne distingue pas les processus et les thread au niveau du noyau, c'est juste le contexte d’exécution qui fait que la mémoire est partagée ou pas. En fait le fork() ou le pthread_create() font appel au même appel système clone(). Mais pour respecter la norme POSIX, le noyau Linux a introduit les particularités suivantes:
  • TGID : Thread Group Identifier qui est initialisé avec le PID du parent,
  • getpid() : retourne le TGID et non le PID,
  • gettid() : retourne le PID.


  • Conclusion

    Voila vous avez les bases pour commencer à coder des scripts en multi-threading! Attention de bien sécuriser vos variables globales.





    Pseudonyme (obligatoire) :
    Adresse mail (obligatoire) :
    Site web :




    © 2017 www.doritique.fr par Robert DORIGNY