Linux — Synchronisation des threads avec POSIX Threads

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 54
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Linux — Synchronisation des threads avec POSIX Threads

Post by Hydraxx »

Linux — Synchronisation des threads avec POSIX Threads

Mutex, variables de condition, deadlocks, initialisation, attributs et modèles d’utilisation

Ce cours présente les principaux mécanismes de synchronisation de POSIX Threads : les mutex et les variables de condition.

Plusieurs threads d’un même processus partagent le même espace d’adressage. Ils peuvent donc accéder aux mêmes variables globales, structures, fichiers et ressources. Sans synchronisation, leurs opérations peuvent s’entrelacer et produire des résultats incohérents.

Les API sont déclarées dans :

Code: Select all

#include <pthread.h>
Compilation :

Code: Select all

gcc programme.c -pthread -o programme
Les fonctions Pthreads renvoient généralement :
  • Code: Select all

    0
    en cas de succès ;
  • un numéro d’erreur positif en cas d’échec.
Elles ne suivent donc pas toujours la convention classique « -1 et errno ».

1. Conditions de concurrence

Une instruction C simple n’est pas forcément atomique.

Exemple :

Code: Select all

static volatile int glob = 0;

glob++;
L’incrémentation peut être décomposée ainsi :
  1. charger

    Code: Select all

    glob
    dans un registre ;
  2. incrémenter cette copie ;
  3. réécrire le résultat dans

    Code: Select all

    glob
    .
Scénario possible :
  • Code: Select all

    glob
    vaut 2000 ;
  • le thread A lit 2000 ;
  • le thread A est interrompu ;
  • le thread B incrémente la variable jusqu’à 3000 ;
  • le thread A reprend et écrit 2001 ;
  • les modifications du thread B sont perdues.
Le résultat dépend alors de l’ordre d’exécution des threads. C’est une condition de concurrence, ou race condition.

volatile ne protège pas les données

Le mot-clé

Code: Select all

volatile
peut empêcher certaines optimisations du compilateur, mais ne garantit pas :
  • l’atomicité ;
  • l’exclusion mutuelle ;
  • la cohérence entre threads ;
  • l’absence d’interruption ;
  • la protection d’un invariant partagé.
À retenir :

Code: Select all

volatile
ne remplace jamais un mécanisme de synchronisation.
2. Sections critiques

Une section critique est une portion de code qui accède à une ressource partagée et qui ne doit pas être exécutée simultanément par plusieurs threads.

Modèle général :

Code: Select all

verrouiller();

/* accès à la ressource partagée */

deverrouiller();
Un mutex possède deux états :
  • déverrouillé : aucun thread ne le possède ;
  • verrouillé : un thread en est propriétaire.
Le mutex ne protège pas automatiquement une variable. Tous les accès concurrents concernés doivent utiliser le même mutex.

3. Type et initialisation d’un mutex

Le type POSIX est :

Code: Select all

pthread_mutex_t
Initialisation statique :

Code: Select all

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
Cette méthode convient aux mutex globaux ou statiques utilisant les attributs par défaut.

4. pthread_mutex_lock() et pthread_mutex_unlock()

pthread_mutex_lock()

Prototype :

Code: Select all

int pthread_mutex_lock(pthread_mutex_t *mutex);
Si le mutex est libre, le thread l’acquiert immédiatement. S’il appartient à un autre thread, le thread appelant est bloqué jusqu’à sa libération.

Code: Select all

int s = pthread_mutex_lock(&mtx);

if (s != 0) {
    errExitEN(s, "pthread_mutex_lock");
}
pthread_mutex_unlock()

Prototype :

Code: Select all

int pthread_mutex_unlock(pthread_mutex_t *mutex);
Cette fonction libère le mutex :

Code: Select all

s = pthread_mutex_unlock(&mtx);

if (s != 0) {
    errExitEN(s, "pthread_mutex_unlock");
}
Le mutex doit normalement être libéré par le thread qui l’a verrouillé.

Exemple complet :

Code: Select all

static int glob = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void *
threadFunc(void *arg)
{
    int loops = *((int *) arg);

    for (int j = 0; j < loops; j++) {
        int s = pthread_mutex_lock(&mtx);
        if (s != 0)
            errExitEN(s, "pthread_mutex_lock");

        int loc = glob;
        loc++;
        glob = loc;

        s = pthread_mutex_unlock(&mtx);
        if (s != 0)
            errExitEN(s, "pthread_mutex_unlock");
    }

    return NULL;
}
La lecture, l’incrémentation et l’écriture sont maintenant exécutées dans une section critique.

5. pthread_mutex_trylock()

Prototype :

Code: Select all

int pthread_mutex_trylock(pthread_mutex_t *mutex);
Cette fonction tente d’acquérir le mutex sans bloquer.

Valeurs importantes :
Exemple :

Code: Select all

int s = pthread_mutex_trylock(&mtx);

if (s == 0) {
    /* section critique */
    pthread_mutex_unlock(&mtx);
} else if (s == EBUSY) {
    /* effectuer un autre travail */
} else {
    errExitEN(s, "pthread_mutex_trylock");
}
Cette API peut servir lorsqu’un thread préfère effectuer un autre travail plutôt que d’attendre.

Elle peut aussi éviter certains deadlocks : le thread verrouille un premier mutex, tente le second avec

Code: Select all

trylock()
, puis libère le premier si le second est indisponible.

Cette stratégie peut toutefois produire un livelock si plusieurs threads se gênent continuellement sans progresser.

6. pthread_mutex_timedlock()

Prototype :

Code: Select all

int pthread_mutex_timedlock(
    pthread_mutex_t *restrict mutex,
    const struct timespec *restrict abs_timeout
);
Cette fonction attend le mutex jusqu’à une date limite absolue.

En cas d’expiration :

Code: Select all

ETIMEDOUT
est renvoyé.

La date est représentée par :

Code: Select all

struct timespec {
    time_t tv_sec;
    long   tv_nsec;
};
Exemple :

Code: Select all

struct timespec limit;

/* remplir limit avec une date absolue */

int s = pthread_mutex_timedlock(&mtx, &limit);

if (s == 0) {
    /* mutex acquis */
    pthread_mutex_unlock(&mtx);
} else if (s == ETIMEDOUT) {
    /* délai expiré */
}
7. Performances et granularité

Sur Linux, NPTL utilise notamment les futexes. En l’absence de contention, un mutex peut souvent être acquis et libéré en espace utilisateur. Le noyau intervient surtout lorsqu’un thread doit être endormi ou réveillé.

Le coût dépend principalement :
  • du nombre de threads en concurrence ;
  • de la durée de la section critique ;
  • du nombre de changements de contexte ;
  • de la fréquence des verrouillages ;
  • de la granularité choisie.
Verrouillage grossier

Un mutex unique protège une grande ressource.

Avantages :
  • simple ;
  • peu de risques de deadlock ;
  • facile à maintenir.
Inconvénient principal : parallélisme limité.

Verrouillage fin

Plusieurs mutex protègent différentes parties d’une structure.

Avantages :
  • meilleur parallélisme ;
  • moins de contention sur un verrou unique.
Inconvénients :
  • code plus complexe ;
  • risque accru de deadlock ;
  • gestion plus difficile de la durée de vie des verrous.
Règle pratique : un mutex protège une ressource ou un invariant partagé, pas seulement une variable isolée.
8. Deadlocks

Un deadlock apparaît lorsque plusieurs threads attendent indéfiniment des ressources détenues les uns par les autres.

Exemple :

Code: Select all

/* Thread A */
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);

/* Thread B */
pthread_mutex_lock(&mutex2);
pthread_mutex_lock(&mutex1);
Le thread A peut détenir

Code: Select all

mutex1
et attendre

Code: Select all

mutex2
, tandis que le thread B détient

Code: Select all

mutex2
et attend

Code: Select all

mutex1
.

Hiérarchie de verrouillage

La solution la plus simple est d’imposer un ordre global :

Code: Select all

mutex1 -> mutex2 -> mutex3
Tous les threads doivent respecter cet ordre.

Code: Select all

pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
Jamais l’inverse.

Approche avec trylock()

Code: Select all

pthread_mutex_lock(&mutex1);

int s = pthread_mutex_trylock(&mutex2);

if (s == EBUSY) {
    pthread_mutex_unlock(&mutex1);
    /* attendre ou effectuer un autre travail */
}
Cette approche est moins simple qu’une hiérarchie de verrouillage, mais peut être utile lorsque l’ordre ne peut pas être imposé facilement.

9. Initialisation et destruction dynamiques

pthread_mutex_init()

Prototype :

Code: Select all

int pthread_mutex_init(
    pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr
);
Exemple :

Code: Select all

pthread_mutex_t mtx;

int s = pthread_mutex_init(&mtx, NULL);

if (s != 0)
    errExitEN(s, "pthread_mutex_init");
L’initialisation dynamique est utile lorsqu’un mutex :
  • est automatique ;
  • est stocké dans une structure allouée dynamiquement ;
  • doit utiliser des attributs particuliers.
pthread_mutex_destroy()

Prototype :

Code: Select all

int pthread_mutex_destroy(pthread_mutex_t *mutex);
Exemple :

Code: Select all

int s = pthread_mutex_destroy(&mtx);

if (s != 0)
    errExitEN(s, "pthread_mutex_destroy");
Le mutex ne doit être détruit que lorsqu’il est déverrouillé et qu’aucun thread ne l’utilise ni ne l’attend.

10. Attributs des mutex

Le type d’attributs est :

Code: Select all

pthread_mutexattr_t
API :

Code: Select all

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_mutexattr_settype(
    pthread_mutexattr_t *attr,
    int type
);

int pthread_mutexattr_gettype(
    const pthread_mutexattr_t *restrict attr,
    int *restrict type
);
Exemple :

Code: Select all

pthread_mutexattr_t attr;
pthread_mutex_t mtx;

pthread_mutexattr_init(&attr);

pthread_mutexattr_settype(
    &attr,
    PTHREAD_MUTEX_ERRORCHECK
);

pthread_mutex_init(&mtx, &attr);
pthread_mutexattr_destroy(&attr);
L’objet d’attributs peut être détruit après l’initialisation du mutex, car les propriétés ont été copiées.

11. Types de mutex

PTHREAD_MUTEX_NORMAL

Mutex rapide avec peu de vérifications.

Si un thread le verrouille deux fois :

Code: Select all

pthread_mutex_lock(&mtx);
pthread_mutex_lock(&mtx);
il peut se bloquer lui-même.

PTHREAD_MUTEX_ERRORCHECK

Ce type détecte certaines erreurs :
  • double verrouillage par le même thread ;
  • déverrouillage par un thread non propriétaire ;
  • déverrouillage d’un mutex non verrouillé.
Erreurs courantes :
  • Code: Select all

    EDEADLK
    : double verrouillage détecté ;
  • Code: Select all

    EPERM
    : mauvais propriétaire.
Il est utile pendant le développement et le débogage.

PTHREAD_MUTEX_RECURSIVE

Le même thread peut verrouiller le mutex plusieurs fois :

Code: Select all

pthread_mutex_lock(&mtx);
pthread_mutex_lock(&mtx);
Le mutex maintient un compteur interne. Il doit être libéré autant de fois :

Code: Select all

pthread_mutex_unlock(&mtx);
pthread_mutex_unlock(&mtx);
Le mutex n’est réellement disponible que lorsque le compteur revient à zéro.

PTHREAD_MUTEX_DEFAULT

Ce type demande le comportement par défaut de l’implémentation. Il ne faut pas dépendre de vérifications particulières sans choisir explicitement un autre type.

Initialisateurs non portables

Certaines implémentations proposent :

Code: Select all

PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP
Le suffixe

Code: Select all

_NP
signifie non portable.

Pour du code portable, utiliser :

Code: Select all

pthread_mutexattr_init();
pthread_mutexattr_settype();
pthread_mutex_init();
12. Variables de condition

Un mutex protège une donnée, mais ne permet pas à lui seul d’attendre efficacement qu’un état devienne vrai.

Exemple de prédicat :

Code: Select all

avail > 0
Une boucle active gaspillerait du temps processeur :

Code: Select all

for (;;) {
    pthread_mutex_lock(&mtx);

    if (avail > 0)
        break;

    pthread_mutex_unlock(&mtx);
}
Une variable de condition permet au thread de dormir jusqu’à ce qu’un autre thread annonce qu’un état partagé a peut-être changé.

Le type est :

Code: Select all

pthread_cond_t
Une variable de condition est associée à :
  • une donnée partagée ;
  • un prédicat ;
  • un mutex protégeant ce prédicat.
Important : la variable de condition ne contient pas l’état. Elle transmet seulement une notification.
13. Initialisation statique

Code: Select all

static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Exemple :

Code: Select all

static pthread_mutex_t mtx =
    PTHREAD_MUTEX_INITIALIZER;

static pthread_cond_t cond =
    PTHREAD_COND_INITIALIZER;

static int avail = 0;
14. pthread_cond_signal() et pthread_cond_broadcast()

pthread_cond_signal()

Prototype :

Code: Select all

int pthread_cond_signal(pthread_cond_t *cond);
Réveille un thread en attente :

Code: Select all

pthread_cond_signal(&cond);
Si aucun thread n’attend, le signal n’est pas mémorisé.

pthread_cond_broadcast()

Prototype :

Code: Select all

int pthread_cond_broadcast(pthread_cond_t *cond);
Réveille tous les threads en attente :

Code: Select all

pthread_cond_broadcast(&cond);
Les threads réveillés doivent ensuite reprendre le mutex. Ils ne poursuivent donc pas tous simultanément dans la section critique.

15. pthread_cond_wait()

Prototype :

Code: Select all

int pthread_cond_wait(
    pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex
);
Le thread doit déjà posséder le mutex :

Code: Select all

pthread_mutex_lock(&mtx);
pthread_cond_wait(&cond, &mtx);
La fonction effectue atomiquement :
  1. la libération du mutex ;
  2. la mise en attente du thread ;
  3. l’attente d’une notification ;
  4. la reprise du mutex ;
  5. le retour au code appelant.
Quand

Code: Select all

pthread_cond_wait()
retourne, le thread possède à nouveau le mutex.

Cette atomicité évite de perdre une notification entre la libération du verrou et la mise en attente.

16. pthread_cond_timedwait()

Prototype :

Code: Select all

int pthread_cond_timedwait(
    pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex,
    const struct timespec *restrict abstime
);
Cette fonction attend jusqu’à une date limite absolue.

En cas d’expiration :

Code: Select all

ETIMEDOUT
est renvoyé.

Même en cas de timeout, le mutex est repris avant le retour.

17. Toujours utiliser une boucle while

Modèle correct :

Code: Select all

pthread_mutex_lock(&mtx);

while (avail == 0) {
    pthread_cond_wait(&cond, &mtx);
}

/* avail > 0 et mutex détenu */

avail--;

pthread_mutex_unlock(&mtx);
Il ne faut pas utiliser un simple

Code: Select all

if
:

Code: Select all

if (avail == 0) {
    pthread_cond_wait(&cond, &mtx);
}
Pourquoi ?
  • un autre thread peut consommer la ressource avant le thread réveillé ;
  • Code: Select all

    pthread_cond_broadcast()
    peut réveiller plusieurs threads ;
  • des réveils intempestifs sont autorisés ;
  • un signal signifie seulement que l’état a peut-être changé.
Règle essentielle : après chaque réveil, le thread doit revérifier le prédicat sous le mutex.
18. Producteur-consommateur

Producteur

Code: Select all

pthread_mutex_lock(&mtx);

avail++;

pthread_mutex_unlock(&mtx);

pthread_cond_signal(&cond);
Il est également possible de signaler avant le déverrouillage :

Code: Select all

pthread_mutex_lock(&mtx);

avail++;
pthread_cond_signal(&cond);

pthread_mutex_unlock(&mtx);
La modification du prédicat doit toujours être réalisée sous mutex.

Consommateur

Code: Select all

pthread_mutex_lock(&mtx);

while (avail == 0) {
    pthread_cond_wait(&cond, &mtx);
}

avail--;

pthread_mutex_unlock(&mtx);
Rôles :
  • le mutex protège

    Code: Select all

    avail
    ;
  • le prédicat

    Code: Select all

    avail > 0
    décide si le consommateur peut continuer ;
  • la variable de condition évite une attente active.
19. Annulation pendant l’attente

Les fonctions :

Code: Select all

pthread_cond_wait()
pthread_cond_timedwait()
sont des points d’annulation.

Si un thread est annulé pendant l’attente, le mutex est repris avant l’exécution des gestionnaires de nettoyage. Cela permet à un gestionnaire de libérer correctement le mutex.

20. Attendre la terminaison de n’importe quel thread

Code: Select all

pthread_join()
attend un thread précis :

Code: Select all

pthread_join(thread_id, NULL);
POSIX ne fournit pas directement une fonction simple permettant d’attendre n’importe lequel parmi plusieurs threads.

Le chapitre construit ce mécanisme avec :
  • un tableau décrivant les threads ;
  • un état pour chaque thread ;
  • un compteur de threads terminés mais non joints ;
  • un mutex ;
  • une variable de condition.
Exemple d’états :

Code: Select all

enum tstate {
    TS_ALIVE,
    TS_TERMINATED,
    TS_JOINED
};
Structure :

Code: Select all

struct thread {
    pthread_t tid;
    enum tstate state;
    int sleepTime;
};
Variables partagées :

Code: Select all

static pthread_mutex_t threadMutex =
    PTHREAD_MUTEX_INITIALIZER;

static pthread_cond_t threadDied =
    PTHREAD_COND_INITIALIZER;

static int numUnjoined = 0;
Lorsqu’un thread termine :

Code: Select all

pthread_mutex_lock(&threadMutex);

thread[index].state = TS_TERMINATED;
numUnjoined++;

pthread_mutex_unlock(&threadMutex);

pthread_cond_signal(&threadDied);
Le thread principal attend :

Code: Select all

pthread_mutex_lock(&threadMutex);

while (numUnjoined == 0) {
    pthread_cond_wait(&threadDied, &threadMutex);
}
Il cherche un thread dans l’état :

Code: Select all

TS_TERMINATED
puis le marque :

Code: Select all

thread[index].state = TS_JOINED;
numUnjoined--;
Après avoir libéré le mutex :

Code: Select all

pthread_mutex_unlock(&threadMutex);

pthread_join(thread[index].tid, NULL);
Cette technique permet de joindre les threads selon leur ordre réel de terminaison.

21. Initialisation et destruction d’une variable de condition

pthread_cond_init()

Prototype :

Code: Select all

int pthread_cond_init(
    pthread_cond_t *restrict cond,
    const pthread_condattr_t *restrict attr
);
Exemple :

Code: Select all

pthread_cond_t cond;

int s = pthread_cond_init(&cond, NULL);

if (s != 0)
    errExitEN(s, "pthread_cond_init");
pthread_cond_destroy()

Prototype :

Code: Select all

int pthread_cond_destroy(pthread_cond_t *cond);
Exemple :

Code: Select all

int s = pthread_cond_destroy(&cond);

if (s != 0)
    errExitEN(s, "pthread_cond_destroy");
La variable de condition ne doit être détruite que lorsqu’aucun thread ne l’utilise ni ne l’attend.

22. Attributs des variables de condition

Le type est :

Code: Select all

pthread_condattr_t
API :

Code: Select all

int pthread_condattr_init(pthread_condattr_t *attr);

int pthread_condattr_destroy(pthread_condattr_t *attr);
Exemple :

Code: Select all

pthread_condattr_t attr;
pthread_cond_t cond;

pthread_condattr_init(&attr);
pthread_cond_init(&cond, &attr);
pthread_condattr_destroy(&attr);
23. pthread_create() et pthread_join()

pthread_create()

Prototype :

Code: Select all

int pthread_create(
    pthread_t *restrict thread,
    const pthread_attr_t *restrict attr,
    void *(*start_routine)(void *),
    void *restrict arg
);
Exemple :

Code: Select all

pthread_t tid;

int s = pthread_create(
    &tid,
    NULL,
    threadFunc,
    &argument
);
pthread_join()

Prototype :

Code: Select all

int pthread_join(
    pthread_t thread,
    void **retval
);
Exemple :

Code: Select all

pthread_join(tid, NULL);
Cette fonction attend la terminaison du thread ciblé et récupère ses ressources.

24. Récapitulatif de toutes les API

Mutex
  • Code: Select all

    pthread_mutex_lock()
    : verrouille en attendant si nécessaire ;
  • Code: Select all

    pthread_mutex_unlock()
    : libère le mutex ;
  • Code: Select all

    pthread_mutex_trylock()
    : tente sans attendre ;
  • Code: Select all

    pthread_mutex_timedlock()
    : attend jusqu’à une date limite ;
  • Code: Select all

    pthread_mutex_init()
    : initialise dynamiquement ;
  • Code: Select all

    pthread_mutex_destroy()
    : détruit le mutex.
Attributs des mutex
Variables de condition
  • Code: Select all

    pthread_cond_wait()
    : attend en libérant atomiquement le mutex ;
  • Code: Select all

    pthread_cond_timedwait()
    : attend avec une date limite ;
  • Code: Select all

    pthread_cond_signal()
    : réveille un thread ;
  • Code: Select all

    pthread_cond_broadcast()
    : réveille tous les threads ;
  • Code: Select all

    pthread_cond_init()
    : initialise dynamiquement ;
  • Code: Select all

    pthread_cond_destroy()
    : détruit la variable de condition.
Attributs des variables de condition
Gestion des threads
  • Code: Select all

    pthread_create()
    : crée un thread ;
  • Code: Select all

    pthread_join()
    : attend et récupère un thread.
25. Types, constantes et erreurs

Types :

Code: Select all

pthread_mutex_t
pthread_mutexattr_t
pthread_cond_t
pthread_condattr_t
pthread_t
Initialisateurs :

Code: Select all

PTHREAD_MUTEX_INITIALIZER
PTHREAD_COND_INITIALIZER
Types de mutex :

Code: Select all

PTHREAD_MUTEX_NORMAL
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_DEFAULT
Extensions non portables :

Code: Select all

PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP
Erreurs principales :
26. Erreurs fréquentes

Utiliser volatile comme verrou

Incorrect :

Code: Select all

volatile int glob;
glob++;
Correct :

Code: Select all

pthread_mutex_lock(&mtx);
glob++;
pthread_mutex_unlock(&mtx);
Protéger seulement les écritures

Les lectures peuvent aussi participer à une race condition. Tous les accès liés au même invariant doivent respecter le même mutex.

Utiliser if au lieu de while

Incorrect :

Code: Select all

if (!predicate)
    pthread_cond_wait(&cond, &mtx);
Correct :

Code: Select all

while (!predicate)
    pthread_cond_wait(&cond, &mtx);
Modifier le prédicat sans mutex

Incorrect :

Code: Select all

avail++;
pthread_cond_signal(&cond);
Correct :

Code: Select all

pthread_mutex_lock(&mtx);
avail++;
pthread_mutex_unlock(&mtx);

pthread_cond_signal(&cond);
Détruire un objet encore utilisé

Ne jamais détruire un mutex ou une variable de condition tant qu’un thread peut encore l’utiliser.

Garder un mutex trop longtemps

Éviter sous mutex :
  • les entrées-sorties lentes ;
  • les attentes réseau ;
  • les calculs indépendants très longs ;
  • les appels bloquants inutiles.
27. Correspondance avec Windows

Correspondances conceptuelles :
La logique est identique :

Code: Select all

verrouiller
tester le prédicat dans while
attendre en libérant atomiquement le verrou
reprendre le verrou
modifier ou consommer l’état
déverrouiller
28. Exercices du chapitre

Observer l’ordonnancement

Modifier le programme d’incrémentation afin que chaque boucle affiche :
  • la valeur de la variable globale ;
  • un identifiant du thread.
Rediriger la sortie dans un fichier et observer les alternances décidées par l’ordonnanceur.

Arbre binaire thread-safe

Implémenter :

Code: Select all

initialize(tree);
add(tree, key, value);
delete(tree, key);
lookup(tree, key);
Première solution : un mutex global pour tout l’arbre.

Solution avancée : plusieurs mutex associés aux nœuds, avec une hiérarchie de verrouillage claire pour éviter les deadlocks.

29. Modèles essentiels

Protéger une donnée

Code: Select all

pthread_mutex_lock(&mtx);

/* accès à la donnée partagée */

pthread_mutex_unlock(&mtx);
Attendre un prédicat

Code: Select all

pthread_mutex_lock(&mtx);

while (!predicate) {
    pthread_cond_wait(&cond, &mtx);
}

/* predicate vraie, mutex détenu */

pthread_mutex_unlock(&mtx);
Modifier l’état et réveiller un thread

Code: Select all

pthread_mutex_lock(&mtx);

modifier_etat_partage();

pthread_mutex_unlock(&mtx);

pthread_cond_signal(&cond);
Réveiller tous les threads

Code: Select all

pthread_mutex_lock(&mtx);

modifier_etat_partage();

pthread_mutex_unlock(&mtx);

pthread_cond_broadcast(&cond);
Conclusion

Les mutex et les variables de condition résolvent deux problèmes complémentaires.

Le mutex protège les données partagées et garantit l’exclusion mutuelle. La variable de condition permet à un thread d’attendre efficacement qu’un prédicat devienne vrai.

Règles essentielles :
  • ne pas utiliser

    Code: Select all

    volatile
    comme mécanisme de synchronisation ;
  • protéger tous les accès liés au même invariant avec le même mutex ;
  • garder les sections critiques aussi courtes que possible ;
  • toujours tester le prédicat dans une boucle

    Code: Select all

    while
    ;
  • modifier le prédicat sous mutex ;
  • considérer un signal comme une invitation à revérifier l’état ;
  • verrouiller plusieurs mutex dans un ordre constant ;
  • ne détruire un objet que lorsqu’aucun thread ne peut encore l’utiliser.
Principe fondamental : le mutex protège l’état partagé, la variable de condition annonce un changement potentiel et le prédicat décide si le thread peut continuer.
Initialisateurs statiques

Code: Select all

PTHREAD_MUTEX_INITIALIZER
PTHREAD_COND_INITIALIZER
Types de mutex

Code: Select all

PTHREAD_MUTEX_NORMAL
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_DEFAULT
Extensions non portables

Code: Select all

PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP
Erreurs importantes
  • Code: Select all

    EBUSY
    : mutex indisponible avec

    Code: Select all

    pthread_mutex_trylock()
    ;
  • Code: Select all

    ETIMEDOUT
    : date limite atteinte ;
  • Code: Select all

    EDEADLK
    : deadlock détecté avec certains types de mutex ;
  • Code: Select all

    EPERM
    : opération interdite, par exemple mauvais propriétaire ;
  • Code: Select all

    EINVAL
    : objet ou paramètre invalide selon l’API.
28. Erreurs de conception fréquentes

28.1 Utiliser volatile comme mécanisme de synchronisation

Incorrect :

Code: Select all

volatile int glob;
glob++;
Correction :

Code: Select all

pthread_mutex_lock(&mtx);
glob++;
pthread_mutex_unlock(&mtx);
28.2 Oublier de protéger certains accès

Si les écritures utilisent le mutex mais pas les lectures, une race condition peut toujours exister.

Tous les accès qui participent au même invariant doivent suivre le même protocole.

28.3 Attendre avec if au lieu de while

Incorrect :

Code: Select all

if (!predicate) {
    pthread_cond_wait(&cond, &mtx);
}
Correct :

Code: Select all

while (!predicate) {
    pthread_cond_wait(&cond, &mtx);
}
28.4 Modifier le prédicat sans mutex

Incorrect :

Code: Select all

avail++;
pthread_cond_signal(&cond);
Correct :

Code: Select all

pthread_mutex_lock(&mtx);
avail++;
pthread_mutex_unlock(&mtx);

pthread_cond_signal(&cond);
28.5 Détruire un objet encore utilisé

Il ne faut pas appeler :

Code: Select all

pthread_mutex_destroy()
pthread_cond_destroy()
tant que des threads peuvent encore utiliser ou attendre sur l’objet.

28.6 Verrouiller plusieurs mutex dans des ordres différents

Tous les threads doivent respecter une hiérarchie de verrouillage commune.

28.7 Garder un mutex trop longtemps

Une section critique doit rester aussi courte que possible, sans sacrifier la cohérence.

Il faut éviter d’effectuer sous mutex :
  • des entrées-sorties lentes ;
  • des attentes réseau ;
  • des calculs indépendants très longs ;
  • des appels susceptibles de bloquer inutilement.
29. Correspondance conceptuelle avec Windows

Les mécanismes POSIX étudiés sont proches de ceux disponibles sous Windows.
  • Code: Select all

    pthread_mutex_t
    correspond conceptuellement à un mécanisme d’exclusion mutuelle comme

    Code: Select all

    CRITICAL_SECTION
    , un

    Code: Select all

    SRWLOCK
    exclusif ou, selon le besoin, un objet Mutex du noyau ;
  • Code: Select all

    pthread_cond_t
    correspond à

    Code: Select all

    CONDITION_VARIABLE
    ;
  • Code: Select all

    pthread_cond_wait()
    joue un rôle proche de

    Code: Select all

    SleepConditionVariableCS()
    ou

    Code: Select all

    SleepConditionVariableSRW()
    ;
  • Code: Select all

    pthread_cond_signal()
    correspond conceptuellement à

    Code: Select all

    WakeConditionVariable()
    ;
  • Code: Select all

    pthread_cond_broadcast()
    correspond conceptuellement à

    Code: Select all

    WakeAllConditionVariable()
    .
La logique reste la même :

Code: Select all

verrouiller
tester le prédicat dans while
attendre en libérant atomiquement le verrou
reprendre le verrou
modifier ou consommer l’état
déverrouiller
30. Exercices proposés par le chapitre

30.1 Observer l’ordonnancement des threads

Modifier le programme d’incrémentation concurrente afin que chaque itération affiche :
  • la valeur actuelle de la variable globale ;
  • un identifiant permettant de reconnaître le thread.
Rediriger ensuite la sortie dans un fichier et observer les alternances d’exécution décidées par l’ordonnanceur.

L’objectif est de visualiser que l’exécution des threads peut s’entrelacer de manière imprévisible.

30.2 Arbre binaire thread-safe

Implémenter des fonctions thread-safe opérant sur un arbre binaire non équilibré :

Code: Select all

initialize(tree);
add(tree, key, value);
delete(tree, key);
lookup(tree, key);
Première solution :
  • utiliser un mutex global protégeant tout l’arbre ;
  • solution simple et sûre ;
  • parallélisme limité.
Solution plus avancée :
  • associer des verrous aux nœuds ;
  • autoriser plusieurs recherches simultanées ;
  • gérer avec prudence les ajouts, suppressions et déplacements entre nœuds ;
  • définir une hiérarchie de verrouillage pour éviter les deadlocks.
31. Modèles essentiels à mémoriser

31.1 Protéger une donnée

Code: Select all

pthread_mutex_lock(&mtx);

/* lire ou modifier la donnée partagée */

pthread_mutex_unlock(&mtx);
31.2 Attendre qu’un prédicat devienne vrai

Code: Select all

pthread_mutex_lock(&mtx);

while (!predicate) {
    pthread_cond_wait(&cond, &mtx);
}

/* predicate est vraie et le mutex est détenu */

pthread_mutex_unlock(&mtx);
31.3 Modifier l’état et réveiller un thread

Code: Select all

pthread_mutex_lock(&mtx);

modifier_etat_partage();

pthread_mutex_unlock(&mtx);

pthread_cond_signal(&cond);
31.4 Modifier l’état et réveiller tous les threads

Code: Select all

pthread_mutex_lock(&mtx);

modifier_etat_partage();

pthread_mutex_unlock(&mtx);

pthread_cond_broadcast(&cond);
Conclusion

Les threads partagent facilement les données, mais cet accès concurrent doit être contrôlé.

Le mutex assure l’exclusion mutuelle et protège les invariants partagés. La variable de condition permet à un thread d’attendre efficacement qu’un état devienne vrai, sans gaspiller du temps processeur dans une boucle active.

Les règles essentielles sont :
  • ne jamais utiliser

    Code: Select all

    volatile
    comme remplacement d’un mécanisme de synchronisation ;
  • protéger tous les accès liés au même invariant avec le même mutex ;
  • réduire autant que possible la durée des sections critiques ;
  • toujours tester le prédicat dans une boucle

    Code: Select all

    while
    ;
  • modifier le prédicat sous mutex ;
  • considérer un signal comme une invitation à revérifier l’état ;
  • verrouiller plusieurs mutex dans un ordre global constant ;
  • ne détruire un mutex ou une variable de condition que lorsqu’aucun thread ne peut encore l’utiliser.
Principe fondamental : le mutex protège l’état partagé, la variable de condition annonce un changement potentiel, et le prédicat décide si le thread peut continuer.

Who is online

Users browsing this forum: No registered users and 1 guest