7 - Les threads

7.1 - Introduction

Dans les versions classiques du système UNIX, l'un des concepts fondamentaux est celui de processus. Un processus recouvre à la fois un ensemble de ressources (essentiellement un espace d'adressage) et une unité d'exécution unique utilisant ces ressources : on y trouve le programme que le processus exécute, des données systèmes (priorité, propriétaires, fichiers ouverts, ...) et l'ensemble des données que le programme manipule, cet ensemble pouvant être lui-même divisé entre les données proprement dites (correspondant aux données statiques et au tas d'un programme C) et les données de la pile d’exécution (correspondant aux variables automatiques). Si plusieurs processus peuvent partager physiquement le même programme, ce que l'on désigne par le terme réentrance, chaque processus dispose de ses propres données physiques, même si par le mécanisme de mémoire partagée, il est possible à plusieurs processus de partager physiquement certaines pages de données après qu'ils en aient fait la demande effective.

Avec l'émergence des machines multiprocesseurs et le développement d'applications selon le modèle serveur/client dans lequel le serveur se démultiplie pour mieux servir ses clients, le concept de processus tel qu'il était ainsi défini a rapidement montré ses limites et son inéquation à exploiter cet aspect multiprocesseurs, essentiellement en raison du coût élevé de la création d'un nouveau processus. Cela a tout d'abord conduit à l'intégration, par certains constructeurs, dans des versions UNIX existantes, de mécanismes permettant de séparer les deux aspects des processus, a savoir d'un coté le processus comme unité d'encapsulation des ressources utilisées et d'un autre les unités d’exécution sur cet ensemble : cela a été en particulier le cas de SUN avec le concept de processus léger (lightprocess). Parallèlement, cette idée a fait son chemin puisqu'elle constitue l'une des abstractions de base que l'on retrouve dans les différents exemples de micro-noyaux qui ont été développés.

Comme nous nous placerons dans ce chapitre dans un contexte UNIX où les activités seront créées dans le cadre d'un processus UNIX, nous adopterons les termes de processus pour désigner l'ensemble des ressources et celui d'activités pour désigner les unités d’exécution(les threads).

Même si fondamentalement, l'utilisation de ce mécanisme est particulièrement adaptée d'une part à un contexte temps réel et d'autre part à un environnement multiprocesseurs, son utilisation peut s'avérer bénéfique dans le cas de systèmes monoprocesseurs par l’amélioration qu'elle apporte du point de vue des performances en matière de création de processus (ou plutôt d'activités).

L’intérêt d’intégrer un tel mécanisme dans les systèmes ouverts a conduit à l'élaboration d'une interface standard dans le cadre de POSIX (standard POSIX 1003.4a). Cependant, si dans les micro-noyaux, les activités constituent des objets de base au même titre que les processus, dans la plupart des systèmes ouverts qui proposent actuellement l'interface correspondante, les différentes fonctions constituent une bibliothèque.

Il doit être clair que l'introduction de tels mécanismes, par lesquels des données physiques sont massivement partagées par différentes entités, suppose la disponibilité de mécanismes de synchronisation permettant de régler les conflits provoqués par les accès concurrentiels à ces données par différentes activités à l'intérieur d'une même tâche.

7.1.1 - Processus UNIX et threads

A un processus UNIX est associé un ensemble d'informations constituant différentes structures de données permettant au système d'en assurer le contrôle. Ces différentes informations peuvent être placées dans deux catégories différentes selon qu'elles contribuent au contrôle des ressources du processus ou a celui de son exécution.

Dans la première catégorie, on trouve en particulier : le masque de création des fichiers (umask), les propriétaires et groupes propriétaires réel et effectif, les descripteurs vers les fichiers ouverts, le répertoire de travail et les informations sur l'implantation en mémoire des données du programme.

Dans la seconde catégorie, on trouve : les informations nécessaires à l'ordonnanceur du système (scheduler), la valeur des registres (et en particulier le compteur ordinal), la pile d'exécution et les informations relatives aux signaux. Ainsi les informations de la première catégorie seront partagées par toutes les activités s’exécutant dans le même contexte et chacune disposera de caractéristiques de la seconde catégorie qui lui seront propres.

Dans ce contexte, un processus UNIX correspond à un processus (ou une tache) où ne s’exécute qu'une seule activité.

7.1.2 - Remarque

Nous nous proposons de décrire le fonctionnement général du mécanisme des Posix threads (P-threads) afin de rester le plus portable possible. Pour cette raison, les types et les fonctions propres à l'utilisation des threads commencent par pthread.

7.1.3 - Compilation pour les threads

Sous Linux : cc -D_REENTRANT -lpthread -o <pgr> <pgr.c>

7.2 - Les attributs d’un activité

7.2.1 - Identification d’une activité

Toute activité possède une identification (TID) de type pthread_t (qui correspond généralement au type entier) dans le cadre du processus dans le contexte duquel elle s’exécute. Un appel à la primitive :

 

#include <unistd.h>

pid_t getpid(void);

 

permet à une activité d’obtenir l’identité du processus UNIX auquel elle appartient. Un appel à la fonction :

 

#include <pthread.h>

pthread_t pthread_self(void);

 

renvoie l’identité de l’activité. Dans un contexte où l’identification d’une activité est un nombre entier, un processus UNIX correspond à une activité initiale dont le numéro est 1 et les nouvelles activités (ou activités annexes) créées dans le cadre d’un processus auront comme identités successives 2,3,… Dans ce même contexte, une activité de numéro tid d’un processus de numéro pid pourra être désignée sous la forme pid.tid ce qui permettra, si le système le permet, d’adresser un signal à une activité particulière d’un processus. Ainsi par exemple, la commande : kill –9 12345.3 enverrait le signal 9 à l’activité 3 du processus UNIX de numéro 12345.

7.2.2 - Terminaison d’un activité

Disons dès maintenant que la terminaison de l’activité initiale d’un processus correspond à celle du processus et entraîne donc la libération des ressources correspondant au processus : ceci a pour conséquence d’empêcher la poursuite de l’exécution des autres activités encore actives dans le cadre du processus.

Par ailleurs, un appel à la fonction :

 

#include <pthread.h>

int pthread_equal(pthread_t tid_1, pthread_t tid_2);

 

permet de tester de manière portable (c’est à dire sans hypothèse sur la nature du type thread_t l’égalité des deux identités tid_1 et tid_2 : la valeur renvoyée par la fonction est non nulle si les identités sont égales et nulle sinon.

7.3 - Création et terminaison des activités

7.3.1 - Création d’une activité : pthread_create

Un appel à la fonction :

 

#include <pthread.h>

int pthread_create( pthread_t *p_tid,

pthread_attr_t attr,

void *(*fonction)(void *arg),

void *arg);

 

correspond à la demande de création et d'activation d'une nouvelle activité.

La valeur 0 est renvoyée en cas de réussite et la valeur -1 en cas d'échec. Dans ce cas, la variable errno permet de connaître la nature de l'erreur rencontrée (EAGAIN si le nombre maximal d'activités est atteint et ENOMEM si l'espace mémoire est insuffisant).

 

Le rôle et l'interprétation des différents paramètres sont les suivants :

- Au retour d'un appel réussi, *p_tid a pour valeur l'identité de l'activité nouvellement créée ;

- le paramètre attr définit la valeur des attributs de l'activité. la valeur par défaut de nom symbolique pthread_attr_default était généralement utilisée, dans les dernières implémentation des thread de POSIX cette variable n'est plus utilisée, on la positionnera donc à la valeur NULL;

- le paramètre fonction correspond à la fonction exécutée par l'activité après sa création : il s'agit donc de son point d'entrée (comme la fonction main est la fonction d'un programme appelée au lancement d'une commande). Ce paramètre est un pointeur sur une fonction ayant un argument de type pointeur (le prototype spécifie le pointeur générique void *) et ayant comme valeur un pointeur (le prototype spécifie également le pointeur générique). Un retour de cette fonction correspondra à la terminaison de l'activité correspondante;

- le paramètre arg correspond au paramètre transmis à la fonction précédente appelée au lancement de l'activité.

7.3.2 - Terminaison d'une activité

7.3.2.1 - Terminaison par exit ou _exit

Si une activité quelconque (c'est à dire initiale ou annexe) d'un processus exécute un appel à l'une de ces deux fonctions, toutes les activités dans le processus se terminent.

7.3.2.2 - La fonction pthread_exit

Un appel à la fonction :

 

#include <pthread.h>

int pthread_exit(int *p_status);

 

termine l'activité appelante avec une valeur de retour égale à *p_status. Ce code de retour est accessible aux autres activités dans le même processus au travers de la fonction pthread_join. Rappelons que la sortie de la fonction appelée au lancement d'une activité entraîne automatiquement un appel à la fonction pthread_exit.

Dans le cas où c'est l'activité initiale d'un processus qui réalise un appel à cette fonction, les ressources du processus sont libérées, ce qui entraîne des erreurs à l’exécution des autres activités du processus et donc leur terminaison.

Il est important de noter que lorsqu'une activité se termine, elle ne disparaît pas, contrairement à ce qui se passe avec les processus UNIX où un processus zombi (c'est à dire terminé) disparaît des que son père a réalisé un appel wait ou waitpid pour acquérir le code de retour correspondant à sa terminaison. Une activité peut ainsi être attendue (par un appel à pthread_join) par plusieurs de ses congénères dans le même processus. Les ressources qui ont été allouées à l'activité ne sont pas restituées. En particulier, son numéro et le segment d'adresses correspondant à sa pile d’exécution sont pas récupérés. La destruction (entraînant la restitution des différentes ressources) doit être explicitement demandée au moyen d'un appel à la fonction :

 

#include <pthread.h>

int pthread_detach(pthread_t p_tid);

 

Un tel appel n'interrompt pas une activité en cours d’exécution : elle indique qu'à la terminaison de l'activité ses ressources devront être restituées au processus auquel elle appartient.

7.3.2.3 - Demande d'abandon d'une activité

Une activité d'un processus peut demander l'abandon d'une autre activité du même processus par une appel à la fonction :

 

#include <pthread.h>

int pthread_cancel(pthread_t tid);

 

Le traitement d'une requête d'abandon par une activité dépend du type de comportement qu'elle a sélectionné. Ce comportement est déterminé par la valeur de deux indicateurs particuliers qui peuvent être positionnés ou non :

- Un indicateur général permet, lorsqu'il n'est pas positionné, de différer la prise en compte des requêtes qui deviennent pendantes jusqu’à ce qu'il soit de nouveau positionné (il s'agit d'un mécanisme analogue à celui qui permet de bloquer la délivrance d'un signal). Le changement d'état de l'indicateur général est réalisé par un appel à la fonction :

 

#include <pthread.h>

int pthread_setcancel(pthread etat);

 

Les valeurs reconnues du paramètre état sont CANCEL_ON et CANCEL_OFF et la valeur retournée par la fonction est celle de l'état antérieur. Par défaut, c'est à dire à la création d'une activité, l'indicateur général est positionné;

 

- Un indicateur d'asynchronisme, qui par défaut est non positionné et qui peut être modifié par un appel à la fonction :

 

#include <pthread.h>

int pthread_setasynccancel(pthread_t etat);

 

Le positionnement de cet indicateur n'est significatif que si l'indicateur général l'est également. Son effet est alors de provoquer la prise en compte immédiate d'une requête d'abandon formulée par une autre activité. S'il n'est pas positionné, la prise en compte d'une telle requête est réalisée au premier point de contrôle rencontré. Un tel point peut tout d'abord être explicitement placé dans le corps de l'activité et correspond à un appel à la fonction :

 

#include <pthread.h>

void pthread_test_cancel(void);

 

Par ailleurs un point de contrôle est implicitement associé à un appel de chacune des fonctions pthread_join, pthread_setcancel, pthread_setasynccancel, pthread_cond_wait et pthread_cond_timedwait (pour ces deux dernières fonctions, après la libération du sémaphore d'exclusion mutuelle correspondant à l'appel).

Dans tous les cas, si aucune requête d'abandon de l'activité n'a été formulée ou si l'indicateur général n'est pas positionné, l'activité se poursuit normalement.

7.3.2.4 - La pile de nettoyage d'une activité

A chaque activité est associée une pile d'appels de fonctions à réaliser à sa terminaison provoquée par un appel à pthread_exit ou suite à une requête d'abandon. Cela peut permettre par exemple la libération d'espace mémoire alloué dynamiquement lors de l’exécution de l'activité ou l'impression d'un message spécifique.

La gestion de cette pile est tout d'abord réalisée par la fonction d'empilement:

 

#include <pthread.h>

int pthread_cleanup_push( void (*fonction)(void *arg),

void *arg);

 

qui permet d'empiler un appel à la fonction *fonction avec la valeur du paramètre spécifié. Par ailleurs, la fonction de dépilement :

 

#include <pthread.h>

void pthread_cleanup_pop(int executer);

 

permet de dépiler l'appel qui se trouve au sommet de la pile. Si l'argument à exécuter est non nul, l'appel qui est dépilé est effectivement réalisé et si sa valeur est nulle, il ne l'est pas.

7.4 - Synchronisation des activités

7.4.1 - Introduction

Le concept d'activités, s'il facilite la communication des entités contribuant à une même tâche puisque les différentes activités d'une même tâche partagent par définition leurs données, nécessite de prendre certaines précaution afin d'assurer une utilisation cohérente de ces données. On se trouve confronté ici au même type de problèmes que lors de l'utilisation d'un même segment de mémoire partagée par différents processus. L'implantation des P-Threads intègrent tout d'abord un mécanisme (mutex ou sémaphore d'exclusion mutuelle) permettant de résoudre le problème de l'exclusion mutuelle d'activités. Un certain nombre d'autres mécanismes sont généralement fournis pour permettre la synchronisation de différentes activités à l’intérieur d'une même tache. On retrouve tout d'abord la possibilité de permettre à une activité d'attendre la terminaison d'une autre. Le second mécanisme qui est fourni permet à des activités d'une même tache de se synchroniser sur l’occurrence d’événements : les objets intervenant dans ce mécanisme sont appelés variables de conditions.

7.4.2 - Synchronisation sur terminaison d'une activité : pthread_join

Il est tout d'abord possible à une ou plusieurs activités d'attendre la terminaison d'une activité particulière et de récupérer une valeur de retour correspondant à sa terminaison. Un telle attente est réalisée par un appel à la fonction :

 

#include <pthread.h>

int pthread_join(pthread_t tid, int **status);

 

L'activité appelante suspend alors son exécution jusqu'à ce que l'activité d'identité tid dans le même processus exécute un appel à la fonction pthread_exit ou qu'une autre activité en ait demandé l'abandon par un appel à la fonction pthread_cancel. Si l'activité est déjà terminée, le retour est immédiat. Au retour de la fonction, **status est égale à la valeur de retour de l'activité.

La valeur de retour est 0 en cas de réussite de l'appel et à -1 en cas d'erreur :

EINVAL : identité d'activité incorrecte;

ESRCH : activité inexistante;

EDEADLK : l'attente de l'activité spécifiée conduit à un deadlock.

7.4.3 - LES MUTEX

Un mutex permet l'exclusion mutuelle entre threads.

Un mutex est de type pthread_mutex_t et a un ensemble d'attributs associé de type pthread_mutexattr_t, que nous ne détaillerons pas ici (ils ne sont pas portables). Nous utiliserons l'ensemble par défaut qui est obtenu en passant NULL aux fonctions qui demandent un pointeur sur cet ensemble .

7.4.3.1 - La primitive de création d'un mutex

 

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex_pt, pthread_mutexattr_t *attr) ;

 

Permet de créer un nouveau mutex avec un certain nombre d'attributs.

Paramètres :

- Avant appel : mutex_pt : pointe sur une zone réservée pour contenir le mutex

attr : ensemble d'attributs à affecter au mutex

- Après appel : mutex_pt : pointe sur le mutex nouvellement créé.

Retour : 0 : en cas de succès

!=0 : en cas d'échec et positionnement de la variable errno (EINVAL, ENOMEM, EAGAIN)

7.4.3.2 - La primitive P pour un mutex

L'appel bloquant

 

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex_pt);  

 

Permet, de façon atomique, de réserver un mutex ou d'attendre tant que le mutex est réservé par une autre activité.

 

 

Paramètre : mutex_pt : pointe sur le mutex à réserver

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL, EDEADLCK)

 

L'appel non bloquant

 

#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex_pt);

 

Permet, de façon atomique, de réserver un mutex ou de renvoyer une valeur particulière si le mutex est réservé par une autre activité.

Paramètre : mutex_pt : pointe sur le mutex à réserver

Retour : 1: en cas de réservation

0 : en cas d'échec de la réservation

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL)

7.4.3.3 - La primitive V pour un mutex

 

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex_pt);  

 

Permet de libérer un mutex et de débloquer les activités en attente sur ce mutex.

Paramètre : mutex_pt : pointe sur le mutex à libérer

Retour : 0 : en cas de succès

!=0 et errno = EBUSY : mutex déjà pris

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL)

7.4.3.4 - La primitive de destruction d'un mutex

 

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex_pt);  

 

Permet de détruire un mutex.

Paramètre : mutex_pt : pointe sur le mutex à détruire

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL, EBUSY)

7.4.4 - Les conditions

Une condition est un mécanisme permettant de synchroniser plusieurs activités à l'intérieur d'une section critique. En effet lors de l'attente

sur une condition un mutex est libéré de façon automatique et réservé lors de la sortie d'attente.

 

Une condition est de type pthread_cond_t, et à un certain nombre d'attributs que nous ne détaillerons pas ici (ils ne sont portables).

Nous utiliserons la valeur par défaut qui est obtenu en passant NULL aux fonctions qui attendent un pointeur sur ce type.

7.4.4.1 - La primitive de création d'une condition

 

#include <pthread.h>

int pthread_cond_init(pthread_cond_t * cond_pt, pthread_condattr_t *attr);  

 

Permet de créer une nouvelle condition.

Paramètres :

- Avant appel cond_pt : pointeur sur la zone réservée pour recevoir la condition

attr : attributs à donner à la condition lors de sa création

- Après appel cond_pt : pointe sur la nouvelle condition

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EAGAIN, EINVAL, ENOMEM)

7.4.4.2 - Les primitives d'attente sur une condition

L'attente non bornée

 

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond_pt, pthread_mutex_t * mutex_pt);

 

Appel bloquant qui attend une condition (envoyée par exemple par la primitive pthread_cond_signal). Pendant l'attente le mutex mutex_pt est libéré et une fois la condition reçue le thread courante essaye de reprendre le mutex (elle peut donc rester bloquée en essayant de le reprendre).

 

Paramètres :

cond_pt : pointeur sur la condition à attendre

mutex_pt : pointeur sur le mutex à libérer pendant l'attente (ceci suppose que ce mutex est réservé)

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL, EDEADLK)

 

L'attente bornée

 

#include <pthread.h>

int pthread_cond_timewait(pthread_cond_t *cond_pt, pthread_mutex_t * mutex_pt, struct timespec *abstime);

 

Permet d'attendre une condition en libérant le mutex de la section critique pendant un temps borné.

 

Paramètres :

cond_pt : pointeur sur la condition à attendre

mutex_pt : pointeur sur le mutex à libérer pendant l'attente (ceci suppose que ce mutex est réservé)

abstime : pointeur sur une structure contenant le temps d'attente :

 

struct timespec {

unsigned long tv_sec; /* secondes */

long tv_nsec; /* nanosecondes */

};

 

Retour :

0 : en cas de succès

!=0 : en cas d'expiration avec errno==ETIMEDOUT

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL, EDEADLK)

7.4.4.3 - Les primitives de signal d'une condition

Réveil d'un thread

 

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond_pt);  

 

Permet de réveiller un thread en attente sur une condition.

Paramètre : cond_pt : pointeur sur la condition à signaler

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL)

 

Réveil de toutes les threads

 

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond_pt);

 

Permet de réveiller toutes les threads en attente sur une condition.

Paramètre : cond_pt : pointeur sur la condition à signaler

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL)

 

Remarque :

Contrairement aux signaux, si une condition est signalée alors qu'aucun thread n'est en attente dessus, l'information "la condition a été signalée" est perdue.

7.4.4.4 - La primitive de destruction d'une condition

 

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond_pt);  

 

Permet de détruire les ressources associées à une condition

Paramètre : cond_pt : pointeur sur la condition à détruire

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL, EBUSY)

7.4.5 - Autres mécanisme associés aux threads

7.4.5.1 - Demande explicite de libération du processeur

Un thread peut demander explicitement au cours de son exécution de rendre la main grâce à la primitive suivante :

 

#include <pthread.h>

void sched_yield();

 

7.4.5.2 - Appel unique à une fonction

Avec les threads la norme POSIX propose un mécanisme assurant qu'une fonction est exécutée seulement une fois. Pour cela on utilise une variable de type pthread_once_t, initialisée à la valeur PTHREAD_ONCE_INIT et la primitive est la suivante :

 

#include <pthread.h>

int pthread_once(pthread_once_t *once_block, void (*init_routine)());  

 

Paramètres : once_block : pointeur sur variable initialisée à la valeur pthread_once_init

init_routine : pointeur sur la fonction à n'appeler qu'une fois

Retour : 0 : en cas de succès

!=0 : en cas d'erreur et positionnement de la variable errno (EINVAL)

 

Exemple :

 

#include <pthread.h>

 

static pthread_once_t make_mutex = PTHREAD_ONCE_INIT;

static pthread_mutex_t mutex;

 

void mutex_init ()

{

pthread_mutex_init(&mutex,pthread_mutexattr_default);

}

...

pthread_once (&make_mutex, mutex_init);

...

 

7.5 - Exemple d'utilisation des mutex et des conditions

Dans cet exemple nous proposons une version du problème des trois fumeurs où chaque fumeur attend un produit particulier parmi : papier, tabac, feu (deux fumeurs n'attendant pas le même produit). La seita dépose un produit chaque fois qu'il n'y a plus rien sur la table (condition).

 

 

/* File : pthread_fumeurs.c */

 

#include <stdio.h>

#include <pthread.h>

 

pthread_t pthread_id[3];

pthread_mutex_t mutex[3];

pthread_cond_t cond;

int i;

 

void * fumeur(void *);

 

int main(int c, char *v[])

{

  /* initialisation des trois mutex et de la condition */

  for(i=0;i<3;i++)

    pthread_mutex_init(mutex+i,NULL);

  pthread_cond_init(&cond,NULL);

  

  /* Prise des trois mutex */

  for(i=0;i<3;i++)

    pthread_mutex_lock(mutex+i);

 

  /* creation des 3 threads fumeurs */

  for(i=0;i<3;i++)

    pthread_create(pthread_id+i,NULL,fumeur,(int *)i);

 

  /* Liberer 100 fois un mutex particulier

   et attendre sur la condition */

  for(i=0;i<100;i++)

    pthread_cond_wait(&cond,mutex+(i%3));

 

  for(i=0;i<3;i++)

      pthread_mutex_destroy(mutex+(i%3));

 

  pthread_cond_destroy(&cond);

  exit(0);

}

 

void *fumeur(void *num)

{

  for(;;)

    {

      /* attendre sur un mutex particulier avant de fumer */

      pthread_mutex_lock(mutex+(int)num);

      printf("%d fume\n",(int)num);

 

      /* liberer le mutex */

      pthread_mutex_unlock(mutex+(int)num);

 

      /* prevenir qui j'ai fini de fumer par une condition */

      pthread_cond_signal(&cond);

 

     /* Rendre le processeur pour etre sur que c'est le buraliste

         qui a la main et donc qui reprend le semaphore */

      sched_yield();

    }

}