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.
#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
.
#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é.
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.
%
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.
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.