Page suivante Page précédente Table des matières

14. Les processus légers (Posix 1003.1c)

Les processus classiques d'UNIX possèdent des ressources séparées (espace mémoire, table des fichiers ouverts...). Lorsqu'un nouveau fil d'exécution (processus fils) est créé par fork(), il se voit attribuer une copie des ressources du processus père.

Il s'ensuit deux problèmes :

Il existe des moyens d'atténuer ces problèmes : technique du copy-on-write dans le noyau pour ne dupliquer les pages mémoires que lorsque c'est strictement nécessaire), utilisation de segments de mémoire partagée (IPC) pour mettre des données en commun. Il est cependant apparu utile de définir un mécanisme permettant d'avoir plusieurs fils d'exécution (threads) dans un même espace de ressources non dupliqué : c'est ce qu'on appelle les processus légers. Ces processus légers peuvent se voir affecter des priorités.

On remarquera que la commutation entre deux threads d'un même groupe est une opération économique, puisqu'il n'est pas utile de recharger entièrement la table des pages de la MMU.

Ces processus légers ayant vocation à communiquer entre eux, la norme Posix 1003.1c définit également des mécanismes de synchronisation : exclusion mutuelle (mutex), sémaphores, et conditions.

Remarque : les sémaphores ne sont pas définis dans les bibliothèques de AIX 4.2 et SVR4 d'ATT/Motorola. Ils existent dans Solaris et les bibliothèques pour Linux.

14.1 Threads

#include <pthread.h>

int  pthread_create(pthread_t  *thread, 
                    pthread_attr_t *attr, 
                    void * (*start_routine)(void *), 
                    void * arg);
void pthread_exit(void *retval);
int pthread_join(pthread_t th, void **thread_return);

La fonction pthread_create demande le lancement d'un nouveau processus léger, avec les attributs indiqués par la structure pointée par attr (NULL = attributs par défaut). Ce processus exécutera la fonction start_routine, en lui donnant le pointeur arg en paramètre. L'identifiant du processus léger est rangé à l'endoit pointé par thread.

Ce processus léger se termine (avec un code de retour) lorsque la fonction qui lui est associée se termine par return retcode, ou lorsque le processus léger exécute un pthread_exit (retcode).

La fonction pthread_join permet au processus père d'attendre la fin d'un processus léger, et de récupérer éventuellement son code de retour.

Priorités : Le fonctionnement des processus légers peut être modifié (priorités, algorithme d'ordonnancement, etc.) en manipulant les attributs qui lui sont associés. Voir les fonctions pthread_attr_init, pthread_attr_destroy, pthread_attr_set-detachstate, pthread_attr_getdetachstate, pthread_attr_setschedparam, pthread_attr_getschedparam, pthread_attr_setschedpolicy, pthread_attr_getschedpolicy, pthread_attr_setinheritsched, pthread_attr_getinheritsched, pthread_attr_setscope, pthread_attr_getscope.

14.2 Verrous d'exclusion mutuelle (mutex)

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t   *mutex,   
                       const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_lock(pthread_mutex_t *mutex));
int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

Les verrous d'exclusion mutuelle (mutex) sont créés par pthread_mutex_init. Il en est de différents types (rapides, récursifs, etc.), selon les attributs pointés par le paramètre mutexattr. La valeur par défaut (mutexattr=NULL) fait généralement l'affaire. L'identificateur du verrou est placé dans la variable pointée par mutex.

pthread_mutex_destroy détruit le verrou. pthread_mutex_lock tente de le bloquer (et met le thread en attente si le verrou est déjà bloqué), pthread_mutex_unlock le débloque. pthread_mutex_trylock tente de bloquer le verrou, et échoue si le verrou est déjà bloqué.

14.3 Exemple

Source :


 
  1     /* leger_mutex.c */

  2     #include <pthread.h>
  3     #include <stdio.h>
  4     #include <unistd.h>

  5     struct Donnees {
  6             char *chaine;           /* chaine à écrire       */
  7             int nombre;             /* nombre de répétitions */
  8             int delai;              /* délai entre écritures */
  9     };

 10     pthread_mutex_t verrou;
 11     int numero = 0;

 12     void *ecriture(void *data)
 13     {
 14             int k;
 15             struct Donnees *d = data;
 16             for (k = 0; k < d->nombre; k++) {
 17                     pthread_mutex_lock(&verrou);        /* DEBUT SECTION CRITIQUE */
 18                     numero++;
 19                     printf("[%d] %s\n", numero, d->chaine);
 20                     pthread_mutex_unlock(&verrou);      /* FIN SECTION CRITIQUE */
 21                     sleep(d->delai);
 22             };
 23             return NULL;
 24     }

 25     int main(void)
 26     {
 27             pthread_t t1, t2;
 28             struct Donnees d1, d2;

 29             d1.nombre = 3;
 30             d1.chaine = "Hello";
 31             d1.delai = 1;
 32             d2.nombre = 2;
 33             d2.chaine = "World";
 34             d2.delai = 2;

 35             pthread_mutex_init(&verrou, NULL);

 36             pthread_create(&t1, NULL, ecriture, (void *) &d1);
 37             pthread_create(&t2, NULL, ecriture, (void *) &d2);

 38             pthread_join(t1, NULL);
 39             pthread_join(t2, NULL);

 40             pthread_mutex_destroy(&verrou);

 41             printf(" %d lignes.\n", numero);
 42             exit(0);
 43     }
 

Compilation:

Sous Linux, avec la bibliothèque linuxthreads de Xavier Leroy (INRIA), ce programme doit être compilé avec l'option -D_REENTRANT et la bibliothèque libpthread:

gcc -g -Wall -pedantic -D_REENTRANT leger_mutex.c -o leger_mutex -lpthread

Exécution:

% leger_mutex 
[1] Hello
[2] World
[3] Hello
[4] Hello
[5] World
 5 lignes.
%

14.4 Sémaphores

Les sémaphores, qui font partie de la norme POSIX, ne sont pas implémentés dans toutes les bibliothèques de threads.

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t * sem);

int sem_wait(sem_t * sem);
int sem_post(sem_t * sem);

int sem_trywait(sem_t * sem);
int sem_getvalue(sem_t * sem, int * sval);

Les sémaphores sont créés par sem_init, qui place l'identificateur du sémaphore à l'endroit pointé par sem. La valeur initiale du sémaphore est dans value. Si pshared est nul, le sémaphore est local au processus lourd (le partage de sémaphores entre plusieurs processus lourds n'est pas implémenté dans la version courante de linuxthreads.).

sem_wait et sem_post sont les équivalents respectifs des primitives P et V de Dijkstra. La fonction sem_trywait échoue (au lieu de bloquer) si la valeur du sémaphore est nulle. Enfin, sem_getvalue consulte la valeur courante du sémaphore.

Exercice: Utiliser un sémaphore au lieu d'un mutex pour sécuriser l'exemple.

14.5 Conditions

Les conditions servent à mettre en attente des processus légers derrière un mutex. Une primitive permet de débloquer d'un seul coup tous les threads bloqués par un même condition.

#include <pthread.h>

int pthread_cond_init(pthread_cond_t  *cond, 
                      pthread_condattr_t *cond_attr);
int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_wait(pthread_cond_t *cond,
                      pthread_mutex_t *mutex);

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

Les conditions sont créées par phtread_cond_init, et détruites par phtread_cond_destroy.

Un processus se met en attente en effectuant un phtread_cond_wait (ce qui bloque au passage un mutex). La primitive phtread_cond_broadcast débloque tous les processus qui attendent sur une condition, phtread_cond_signal en débloque un seul.


Page suivante Page précédente Table des matières