2. Synchronisation : organiser l’accès et la progression

Les trois participants doivent atteindre le rendez-vous avant qu’un participant puisse produire sa ligne après la barrière.
- Les trois lignes avant précèdent toutes les lignes après.
- L’ordre des participants à l’intérieur de chaque phase n’est pas garanti.
Lire la capture en texte
barriere = threading.Barrier(3)
# trois threads appellent barriere.wait()
A : avant la barrière
B : avant la barrière
C : avant la barrière
C : après la barrière
A : après la barrière
B : après la barrièreDémonstration Station Minuit · Ubuntu 24.04 / WSL2 · Python 3 · 15 octobre 2026 (Toronto). Sortie réelle remise en page et capturée; exemple ajouté, distinct du matériel officiel.
2.1. Sémaphore : réserver une ressource ou imposer un ordre
Un sémaphore représente des autorisations disponibles. P/down/wait consomme une autorisation si possible; sinon l’appelant attend. V/up/post rend une autorisation et peut rendre prêt un attenteur. Dans le modèle bloquant du cours, l’attente ne tourne pas continuellement sur le CPU. Les noms POSIX sont sem_wait et sem_post; le mot pédagogique « signal » utilisé pour V ne désigne ici ni un signal Unix ni une variable de condition.
Un sémaphore de comptage initialisé à K permet de réserver K ressources. Pour une section exclusive initialement libre, un sémaphore binaire initialisé à 1 permet le patron :
Code (text) · sélectionner pour copier
P(verrou)
section critique
V(verrou)
Quand P1 consomme le jeton, P2 attend à zéro. Après la libération, P2 peut redevenir prêt; l’ordonnanceur décide ensuite quand il s’exécute. Pour protéger, chaque participant encadre son accès par P puis V. Le compteur doit rester cohérent avec les acquisitions et libérations; une opération post superflue peut détruire la protection attendue.
Le même outil impose une dépendance S1 avant S2 avec un sémaphore initialisé à 0 :
Code (text) · sélectionner pour copier
P1 : S1; V(s)
P2 : P(s); S2
Si P2 commence, il attend avant S2. Si P1 commence, son post conserve un jeton que P2 consommera plus tard. Le zéro initial signifie « permission pas encore produite ». Le post et le wait peuvent appartenir à des participants différents. Cette notification croisée est utile pour l’ordre des opérations; un mutex possédé par un thread n’a pas la même fonction.
On initialise les objets avant usage et les détruit après la fin de leurs utilisations. sem_init(&s,0,valeur) indique un partage entre threads du même processus. Les valeurs initiales utilisées dans les exercices sont non négatives; les compteurs négatifs évoqués dans certains modèles d’attente ne sont pas une valeur négative à fournir à sem_init.
2.2. Producteur–consommateur : trois contraintes, trois sémaphores
Une file bornée de N places exige de ne pas produire dans une file pleine, de ne pas consommer dans une file vide, et de ne pas modifier simultanément sa structure. Les trois objets correspondent à ces contraintes :
| Objet | Initialisation | Signification |
|---|---|---|
empty |
N | Places encore disponibles à réserver |
full |
0 | Éléments disponibles à réserver pour consommer |
mutex |
1 | Accès exclusif à l’insertion et au retrait; ici, sémaphore binaire |
full compte les éléments disponibles; il ne signifie pas uniquement que la file entière est pleine. L’état initial est une file vide avec toutes ses places libres. Le patron est :
Code (text) · sélectionner pour copier
Producteur Consommateur
fabriquer un élément P(full)
P(empty) P(mutex)
P(mutex) retirer un élément
insérer l’élément V(mutex)
V(mutex) V(empty)
V(full) traiter l’élément retiré
Chaque côté réserve sa ressource avant de prendre le verrou structurel, utilise la file, libère le verrou, puis rend disponible la ressource attendue par l’autre. Le producteur fournit les éléments attendus sur full; le consommateur fournit les places attendues sur empty. La fabrication et le traitement indépendant se font hors du verrou.
Trois simulations expliquent la correction :
- File vide : full=0 bloque le consommateur. La première insertion suivie de V(full) lui donne une permission; si aucun consommateur n’attend, elle est conservée.
- File pleine : empty=0 bloque le producteur. Un retrait puis V(empty) libèrent une place. Il suffit d’une place; il n’est pas nécessaire de vider toute la file.
- Ressource disponible : une place ou un élément peut être réservé, mais la modification de la file reste exclusive grâce au verrou.
Inverser l’ordre et prendre mutex avant P(empty) crée le risque de blocage suivant : le producteur garde le verrou en attendant une place, mais le consommateur a besoin de ce verrou pour retirer et créer la place. Symétriquement, garder le verrou en attendant full empêcherait le producteur d’insérer. La disponibilité d’une ressource et la protection d’une structure sont donc deux raisons d’attendre différentes.
2.3. Mutex : propriété, mise à jour complète et variantes
Un mutex est spécialisé pour l’exclusion mutuelle, avec un propriétaire. Son rôle est de protéger une opération cohérente sur un état partagé, par exemple l’ensemble « lire le compteur, ajouter 1, écrire » :
Code (c) · sélectionner pour copier
pthread_mutex_lock(&m);
counter++;
pthread_mutex_unlock(&m);
Deux threads effectuant chacun M incréments depuis zéro donnent 2 M si toutes les mises à jour respectent cette protection. Sans elle, deux lectures d’une même valeur peuvent conduire à une écriture écrasée. L’intervalle M à 2 M évoqué pour le mauvais code est un modèle pédagogique d’entrelacement, pas une garantie de C : une course sur des accès ordinaires peut relever du comportement indéfini.
| Fonction | Rôle |
|---|---|
pthread_mutex_init |
Initialiser l’objet et ses attributs |
pthread_mutex_lock |
Acquérir ou attendre |
pthread_mutex_trylock |
Tenter sans attendre et traiter l’indisponibilité |
pthread_mutex_unlock |
Libérer par le propriétaire |
pthread_mutex_destroy |
Détruire après la fin des utilisations |
Le verrou n’interdit pas la préemption de son propriétaire. Il empêche les autres participants qui respectent ce verrou de réaliser l’accès protégé, même si le propriétaire quitte temporairement le CPU. Une notification de sémaphore peut être croisée entre threads; déverrouiller le mutex d’un autre thread n’est pas une méthode de notification valide.
| Type présenté | Réacquisition et libération |
|---|---|
PTHREAD_MUTEX_NORMAL |
Une seconde acquisition par son propriétaire entraîne un interblocage |
PTHREAD_MUTEX_RECURSIVE |
Le propriétaire peut acquérir plusieurs fois; autant de unlock sont nécessaires pour libérer effectivement |
PTHREAD_MUTEX_ERRORCHECK |
Certaines erreurs de réacquisition ou de libération par un non-propriétaire retournent une erreur |
L’absence de contrôle explicite dans un autre type n’autorise pas un non-propriétaire à déverrouiller : le comportement peut être indéfini. Les attributs par défaut ne doivent pas être supposés identiques à tous les comportements NORMAL. De même, la file FIFO utilisée oralement comme exemple n’est pas une garantie générale d’ordre d’acquisition des mutex POSIX; l’implémentation et la politique d’ordonnancement interviennent.
2.4. Priorités, attente active et mutex adaptatif
L’inversion de priorité apparaît quand un demandeur prioritaire dépend d’un détenteur moins prioritaire pour obtenir une ressource. Dans le scénario du cours, le demandeur qui tourne activement peut occuper le CPU au détriment du détenteur qui doit finir pour libérer. L’héritage de priorité élève temporairement la priorité du détenteur au niveau requis, lui permet de terminer sa section, puis restaure sa priorité après la libération. C’est un protocole particulier, pas une propriété automatique de tout mutex, et ce n’est pas une solution universelle aux interblocages.
L’attente active évite parfois le coût du sommeil, du réveil et d’un nouvel ordonnancement, mais consomme du CPU sans travail utile. Le blocage économise ce CPU; le thread réveillé doit néanmoins attendre son tour parmi les prêts. Un mutex adaptatif peut tourner brièvement si le détenteur est exécuté et susceptible de libérer bientôt, puis bloquer si l’attente persiste.
Le raisonnement dépend de la progression réelle du propriétaire. Sur un seul cœur, tourner empêche le détenteur d’avancer jusqu’à un changement d’ordonnancement. Sur plusieurs cœurs, le détenteur peut progresser sur un autre cœur et libérer pendant une brève attente active. S’il est suspendu ou bloqué, ce gain n’est pas attendu. Le cours donne ce principe, sans seuil numérique d’adaptation à mémoriser.
2.5. Barrière : attendre la fin collective d’une phase
Une barrière est un rendez-vous configuré pour un certain nombre de participants. Dans « A; barrière; B », aucun participant ne commence B avant que toutes les arrivées requises aient atteint la barrière après A. Un calcul parallèle par lots peut ainsi attendre que toutes les portions de données soient prêtes avant la phase suivante.
Les fonctions sont pthread_barrier_init, pthread_barrier_wait et pthread_barrier_destroy. L’initialisation précise l’objet, les attributs — NULL pour ceux par défaut — et le compte attendu. Si un participant requis n’atteint jamais wait, les autres peuvent rester bloqués. Détruire ne se fait qu’après la fin des usages.
La barrière ne protège pas automatiquement les accès dans A ou B. Plusieurs threads peuvent y effectuer des mises à jour concurrentes nécessitant encore un mutex. Elle impose une frontière entre phases; elle n’accorde pas une zone à un seul thread comme un verrou.
2.6. Variable de condition : prédicat, mutex et attente atomique
Un sémaphore représente des autorisations; une variable de condition sert à attendre un prédicat défini par le programme : tampon non vide, place libérée, caractère recherché présent, résultat disponible. La condition n’est ni le booléen du prédicat ni l’état durable. Les données partagées portent cet état; le code doit les tester sous mutex.
Code (c) · sélectionner pour copier
pthread_mutex_lock(&mutex);
while (!predicat_sur_etat_partage) {
pthread_cond_wait(&condition, &mutex);
}
/* lire ou modifier l'état protégé */
pthread_mutex_unlock(&mutex);
pthread_cond_wait est appelé avec le mutex déjà détenu. Il libère atomiquement ce mutex et met le thread en attente; avant son retour, il le reprend. Le thread attend donc sans empêcher l’autre de modifier l’état qui le fera progresser. Garder le mutex pendant une attente ordinaire pourrait empêcher précisément cette modification.
La décomposition des diapositives « unlock, suspension, lock » décrit les effets internes. Elle n’est pas un remplacement correct par trois opérations séparées : entre le test et la suspension, l’autre thread pourrait modifier l’état et envoyer une notification perdue. L’atomicité de la transition d’attente est déterminante.
Au retour, le mutex est acquis, mais le prédicat n’est pas garanti vrai. Un autre thread peut avoir pris la ressource avant la réacquisition; des réveils spontanés sont également permis. Le while reteste donc l’état après chaque retour. La notification ne transfère immédiatement ni le CPU ni la possession du mutex.
Le tampon d’une place utilise buf=0 pour libre, des valeurs positives 1 à MAX pour les données, un mutex et deux conditions : condp pour une place et condc pour une donnée.
Code (text) · sélectionner pour copier
Producteur Consommateur
lock(mutex) lock(mutex)
while buf != 0: while buf == 0:
wait(condp, mutex) wait(condc, mutex)
buf = nouvelle_valeur retirer la donnée; buf = 0
signal(condc) signal(condp)
unlock(mutex) unlock(mutex)
Le déroulement i=1/i=2 explique l’attente. Le producteur écrit 1, signale condc et sort. Il peut reprendre le mutex avant le consommateur, constater buf encore égal à 1 à l’itération 2 et appeler wait sur condp. Il libère alors le mutex. Le consommateur retire 1, remet buf à 0, signale condp et sort. Le producteur pourra reprendre le mutex, retester et écrire 2. Un réveil ne garantissait donc pas que le consommateur avait déjà travaillé.
| API | Fonction |
|---|---|
pthread_cond_init, pthread_cond_destroy |
Initialiser puis détruire après usage |
pthread_cond_wait |
Libération/attente atomique, reprise du mutex avant retour |
pthread_cond_signal |
Notifier un attenteur dans le modèle du support; aucun choix précis garanti |
pthread_cond_broadcast |
Notifier tous les attenteurs, qui reprennent chacun le mutex et retestent |
Un signal sans attenteur n’est pas conservé comme jeton pour une future attente. Le programme reste correct grâce à l’état persistant : si la donnée est déjà disponible, le prédicat vrai évite d’attendre. C’est la différence avec le post de sémaphore conservé avant le wait, utilisée au labo pour le démarrage d’un thread pas encore créé.
Le code des diapositives illustre le mécanisme, sans fournir toute l’initialisation et la terminaison d’un programme. Les graphies phthread_exit, les points-virgules manquants et l’alternance buffer/buf sont des coquilles, pas de nouvelles API. Les attributs, délais et codes d’erreur détaillés ne sont pas développés dans cette partie.
La référence officielle Windows transpose le patron avec une section critique ou un verrou SRW. SleepConditionVariableCS et SleepConditionVariableSRW libèrent et attendent atomiquement, puis reprennent le verrou; WakeConditionVariable et WakeAllConditionVariable notifient un ou tous les attenteurs. Le test en boucle reste nécessaire, y compris si un autre thread a pris la ressource. Ces conditions concernent les threads d’un même processus; elles ne remplacent pas les appels Pthreads demandés dans les exercices. Documentation Microsoft.
2.7. Choisir et raccourcir la protection
Le mécanisme doit exprimer le besoin : K autorisations ou notification mémorisée → sémaphore; accès exclusif avec propriété → mutex; fin collective d’une phase → barrière; état partagé arbitraire à attendre → prédicat, mutex et condition. Une application peut combiner plusieurs de ces besoins.
Une longue section critique bloque les autres, même si le détenteur est préempté. Avec une attente active, elle gaspille aussi le CPU. On peut retirer ou copier les données nécessaires sous mutex, libérer, puis traiter hors verrou si ce traitement est indépendant de l’état partagé. L’image du support où un occupant prolonge l’utilisation exclusive d’une toilette pour lire son journal illustre ce maintien inutile. Raccourcir ne signifie pas enlever la protection du test, de la mise à jour ou de la transition d’attente.
Une exécution possible du maître §2.6, sans échelle de temps : buf=0 signifie libre. Le producteur détient le mutex, écrit 1, signale condc puis libère le mutex. Il peut le reprendre avant le consommateur. À i=2, il constate buf=1 et appelle wait(condp,mutex) : libération du mutex et mise en attente sont atomiques. Le consommateur acquiert ensuite le mutex, retire 1, écrit buf=0, signale condp et libère. Le producteur doit être ordonnancé puis reprendre le mutex avant le retour de wait; il reteste while(buf!=0) et écrit 2 seulement si la place est libre. Un réveil ne transfère immédiatement ni CPU ni mutex. Un signal sans attenteur ne se conserve pas comme jeton; c’est buf, testé sous mutex, qui conserve l’état. Les réveils spontanés imposent également le retest. Les rectangles représentent des étapes logiques, pas des durées ni une garantie sur l’ordre d’acquisition.
Source : Maître S06 du 14 octobre 2026 — ajout éditorial WEB-10, sources originales conservées · §2.6, lignes 145–189 ; Concurrence partie 4, tampon i=1/i=2, incorporé depuis S05 ; ED04. Trace d’états vérifiée dans VERIFICATION_WEB10.json.