• Aucun résultat trouvé

Zones d'exclusions mutuelles

Dans le document Programmation système en C sous (Page 156-159)

L'un des enjeux essentiels lors du développement d'applications multithreads est la synchronisation entre les différents fils d'exécution concurrents. Ce qui représente, somme toute, un aspect annexe des logiciels reposant sur plusieurs processus devient ici un point crucial. Les différents threads d'une application disposant d'un accès partagé immédiat à toutes les variables globales, descripteurs de fichiers. etc., leur synchronisation est indispensable pour éviter la corruption de données et les situations de blocage.

Il existe essentiellement deux cas où des données risquent d'être corrompues si l'accès aux ressources communes n'est pas synchronisé :

• Deux threads concurrents veulent modifier une variable globale, par exemple décrémenter un compteur dans une gestion de stocks. Le premier thread lit la valeur initiale V0 dans un registre du processeur. Il décrémente la valeur d'une unité.

L'ordonnanceur commute les tâches et donne la main au second thread. Celui-ci lit la valeur initiale V0, la décrémente et écrit la nouvelle valeur V0 –1 dans le compteur.

L'ordonnanceur réactive le premier thread qui inscrit à son tour la valeur calculée V0-1 dans le compteur. Au final, le stock indique V0–1 unités alors qu'il aurait dû être décrémenté deux fois. Ceci présage de sérieux problèmes le jour de l'inventaire...

• Un thread modifie une structure de données globale tandis qu'un autre essaye de la lire. Le thread lecteur charge les premiers membres de la structure. L'ordonnanceur bascule le contrôle au thread écrivain, qui modifie toute la structure. Lorsque le second thread est réactivé, il lit la fin de la structure. Les premiers membres qu'il a reçus ne sont pas cohérents avec les suivants. Le problème pourrait être le même avec

Pour accéder à des données globales, il est donc indispensable de mettre en oeuvre un mécanisme d'exclusion mutuelle des threads. Ce principe repose sur des données appelées mutex. de type pthread_mutex_t. Chaque variable sert de verrou pour l'accès à une zone particulière de la mémoire globale.

Il existe deux états pour un mutex : disponible ou verrouillé. Lorsqu'un mutex est verrouillé par un thread, on dit que ce dernier tient le mutex. Un mutex ne peut être tenu que par un seul thread à la fois. En conséquence, il existe essentiellement deux fonctions de manipulation des mutex : une fonction de verrouillage et une fonction de libération.

Lorsqu'un thread demande à verrouiller un mutex déjà maintenu par un autre thread, le premier est bloqué jusqu'à ce que le mutex soit libéré.

On peut initialiser un mutex de manière statique ou dynamique, en précisant certains attributs à l'aide d'un objet de type phtread_mutexattr_t. L'initialisation statique se fait à l'aide de la constante PTHREAD_MUTEX_INITIALIZER1

pthread_mutex_t mutex = PHTREAD_MUTEX_INITIALIZER;

Pour l'initialisation dynamique, on emploie pthread_mutex_init( ) avec une variable regroupant les attributs du mutex.

int pthread_mutex_init (pthread_mutex_t * mutex,

const pthread_mutexattr_t * attributs);

On l'emploie généralement ainsi : pthread_mutex_t mutex;

pthread_mutexattr_t mutexattr.

/* initialisation de mutexattr */

[.. ]

/* initialisation du mutex */

if ((mutex = malloc (sizeof (pthread_mutex_t)) == NULL) return (-1);

pthreadmutex_init (& mutex, & mutexattr);

[...]

Étant donné que les mutex servent à synchroniser différents threads, on les déclare naturelle-ment dans des variables globales ou dans des variables locales statiques.

On peut utiliser un pointeur NULL en second argument de pthread_mutex_init( ) si le mutex doit avoir les attributs par défaut. Nous verrons plus bas comment configurer les attributs désirés. Une fois qu'un mutex n'est plus utilisé, on libère la variable en appelant pthread_mutexdestroy( ). Le murex ne doit plus être verrouillé, sinon cette fonction échoue avec l'erreur EBUSY.

int pthread_mutex_destroy (pthread_mutext * mutex);

La fonction de verrouillage s'appelle pthread_mutex_lock( ). Si le murex est libre, il est immédiatement verrouillé et attribué au thread appelant. Si le mutex est déjà maintenu par un autre thread, la fonction reste bloquée jusqu'à la libération du mutex, puis elle le verrouille à

la disposition du thread appelant. Cette fonction peut donc rester bloquée indéfiniment.

Ce n'est pourtant pas un point d'annulation car la norme Posix.1c réclame que l'état des mutex soit parfaitement prévisible lors de l'annulation d'un thread. Si pthread_mutex_lock( ) pouvait être un point d'annulation, l'état du thread serait imprévisible.

Si pthread_mutex_lock( ) est invoquée sur un mutex déjà maintenu par le thread appelant, le résultat dépend du type de mutex — déterminé par les attributs employés lors de l'initialisa-fion :

• Un mutex normal bloque le thread appelant jusqu'à sa libération. Comme celle-ci est impossible, le thread reste bloqué définitivement.

• Si le mutex est de type récursif — extension non portable — le thread le verrouille à nouveau en incrémentant un compteur interne. Il faudra alors débloquer le mutex un nombre égal de fois pour qu'il devienne vraiment disponible.

• Si nous avons à faire à un mutex de diagnostic — également non portable —, la fonction pthread_mutex_lock( ) échoue en renvoyant le code EDEADLOCK qui indique une situation de blocage définitif. Cela permet de rechercher les cas d'erreur lors d'une session de débogage.

Le prototype de pthread_mutex_lock( ) est le suivant : int pthreadmutex_lock (pthread_mutex_t * mutex);

La libération d'un mutex se fait avec la fonction pthread_mutex_unlock( ). Si le mutex est récursif, il ne sera effectivement débloqué que si le compteur interne de verrouillage tombe à zéro. Avec un mutex de diagnostic, une erreur (EPERM) se produit si le thread appelant ne possède pas le mutex. Avec les autres murex, cette vérification n'a pas lieu, mais ce comportement n'est pas standard.

int pthread_mutex_unlock (pthread_mutext * mutex);

Enfin, il existe une fonction nommée pthread_mutex_trylock( ) fonctionnant comme pthread_mutex_lock( ), à la différence qu'elle échoue avec l'erreur EBUSY, plutôt que de rester bloquée, si le mutex est déjà verrouillé.

int pthread_mutex_trylock (pthread_mutex_t * mutex);

Il est généralement déconseillé d'employer pthread_mutex_trylock( ). Notamment, si on désire surveiller plusieurs mutex à la fois, on n'utilisera pas une construction du genre :

while (1) {

if (pthread_mutex_trylock (& mutex_1) == 0) break;

if (pthread_mutex_trylock (& mutex_2) == 0) break;

if (pthread_mutex_trylock (& mutex_3) == 0) break;

}

Ce code est très mauvais car il gâche inutilement des ressources CPU, alors qu'il est

Les attributs d'un mutex, enregistrés dans un objet de type mutex_attr_t, peuvent être initialisés avec la fonction pthread_mutexattr_init( ) et détruits avec pthread_mutexattr_destroy( ).

int pthread_mutexattr_init (pthread_mutexattr_t * attributs);

int pthread_mutexattr_destroy (pthread_mutexattr_t * attributs);

Les variables pthread_mutexattr_t, avec la bibliothèque LinuxThreads, ne comportent qu'un seul attribut, le type de mutex. Cet attribut est spécifique et ne doit pas être employé dans des programmes dont on désire assurer la portabilité.

Pour le configurer, on emploie la fonction pthread_mutexattr_setkind_np( ) et pthread_mutexattr_getkind_np( ) pour le lire.

int pthread_mutexattr_setkind_np (pthread_mutexattr_t * attributs, int type);

int pthread_mutexattr_getkind_np (pthread_mutexattr_t * attributs, int * type);

Le type d'un mutex est représenté par l'une des constantes suivantes :

Nom Signification

PTHREAD_MUTEX_FASTNP Mutex normal, rapide. L'invocation double de pthread_mutex_look( ) dans le même thread conduit à un blocage définitif.

PTHREAD_MUTEX_RECURSIVE_NP Mutex récursif. Un même thread peut le verrouiller à plusieurs reprises. Il faudra le libérer autant de fois.

PTHREAD_MUTEX_ERRORCHECK_NP Mutex de diagnostic. Une tentative de double verrouillage échoue. Le déverrouillage d'un mutex maintenu par un autre thread échoue.

REMARQUE Les types de mutex décrits ci-dessus ainsi que leurs fonctions de configuration et de lecture ne sont pas portables, et ne devront être utilisés qu'avec parcimonie.

Le programme suivant utilise un mutex comme verrou pour restreindre l'accès au flux stdout. Nous lançons en parallèle une dizaine de threads, qui vont attendre une durée aléatoire avant de demander un blocage du mutex. L'attente aléatoire sert à perturber un peu le déterminisme de l'ordonnanceur et à éviter de voir les threads se dérouler dans l'ordre croissant.

exemple_mutex.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pthread.h>

static void * routine threads (void * argument);

static int aleatoire (int maximum);

pthread_mutex_t mutex_stdout = PTHREAD_MUTEX_INITIALIZER;

int main (void) {

int i;

pthread_t thread;

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

pthread_create (& thread, NULL, routine_threads, (void *) i);

pthread_exit (NULL);

}

static void *

routine_threads (void * argument) {

int numero = (int) argument;

int nombre_iterations;

int i;

nombre_iterations = aleatoire (6);

for (i = 0; i < nombre_iterations; i++) { sleep (aleatoire (3));

pthread_mutex_lock (& mutex_stdout);

fprintf (stdout, "Le thread numéro %d tient le mutex \n", numero);

pthread_mutex_unlock (& mutex_stdout);

}

return (NULL);

}

static int

aleatoire (int maximum) {

double d;

d = (double) maximum * rand( );

d = d / (RAND_MAX + 1.0);

return ((int) d);

}

On remarque l'emploi de pthread_exit( ) en fin de fonction main( ) pour terminer le fil d'exécution principal, sans finir les autres threads. Le déroulement du processus montre bien que l'accès est correct, malgré les demandes concurrentes de verrouillage du mutex.

$ ./exemple_mutex

Le thread numéro 2 tient le mutex Le thread numéro 0 tient le mutex Le thread numéro 4 tient le mutex Le thread numéro 5 tient le mutex Le thread numéro 6 tient le mutex Le thread numéro 2 tient le mutex Le thread numéro 0 tient le mutex Le thread numéro 5 tient le mutex Le thread numéro 6 tient le mutex Le thread numéro 0 tient le mutex Le thread numéro 1 tient le mutex Le thread numéro 3 tient le mutex Le thread numéro 7 tient le mutex Le thread numéro 8 tient le mutex Le thread numéro 1 tient le mutex Le thread numéro 3 tient le mutex Le thread numéro 8 tient le mutex Le thread numéro 0 tient le mutex Le thread numéro 2 tient le mutex

Le thread numéro 1 tient le mutex Le thread numéro 2 tient le mutex Le thread numéro 0 tient le mutex Le thread numéro 7 tient le mutex Le thread numéro 8 tient le mutex Le thread numéro 1 tient le mutex Le thread numéro 2 tient le mutex Le thread numéro 7 tient le mutex Le thread numéro 7 tient le mutex Le thread numéro 7 tient le mutex

$

La définition des verrous corrects à employer pour accéder aux données partagées est une tâche importante de la conception des programmes multithreads. En prenant l'exemple d'une grosse base de données — des réservations ferroviaires par exemple —, il serait vraiment peu efficace de verrouiller l'ensemble de la base à chaque fois qu'un thread veut ajouter un enregistrement. D'un autre côté, un trop grand nombre de mutex indépendants peut aussi devenir problématique. Qu'un thread ait systématiquement besoin de verrouiller simultanément plusieurs mutex peut être très dangereux, car la moindre maladresse dans le programme risque de déclencher des blocages irrémédiables. Dans cette situation d'étreinte fatale, un thread maintient un mutex et en attend un autre, alors qu'un autre thread est coincé dans la situation inverse.

Il est donc indispensable de bien dimensionner le problème et de décider de la granularité des portions protégées par un mutex.

Dans le document Programmation système en C sous (Page 156-159)