- Apprendre
- Informatique et numérique
- Informatique
- Systèmes d’exploitation : processus, mémoire et fichiers
- Sémaphores et interblocage
Variables de condition, sémaphores et interblocage
IntermédiaireUnité 6 sur 9 de la formation Formation : Systèmes d’exploitation : processus, mémoire et fichiers
Ce que tu vas apprendre
- Mets un fil d’exécution en sommeil jusqu’à ce qu’une condition portant sur des données partagées soit vraie, en utilisant une variable de condition avec un verrou et une variable d’état, puis utilise ce schéma pour construire un tampon borné correct.
- Attends une condition sans attente activeExplique pourquoi un verrou seul ne peut pas faire attendre un événement à un fil d’exécution et utilise l’attente et le signalement avec un verrou et une variable d’état pour qu’aucun réveil ne soit perdu.
- Vérifie de nouveau les conditions et choisis entre signalement et diffusionApplique la sémantique de Mesa en vérifiant de nouveau la condition dans une boucle while, choisis entre signalement et diffusion et reconnais les mêmes règles dans les moniteurs et en Java.
- Résous le problème des producteurs et des consommateurs avec des variables de conditionConstruis un tampon borné partagé par des fils producteurs et consommateurs, suis les raisons de l’échec des versions naïves et corrige-les avec des boucles while et deux variables de condition.
- Attends une condition sans attente active
- Utilise le sémaphore, une primitive unique, comme verrou, comme signal entre fils d’exécution et comme compteur de ressources, puis applique-le au tampon borné et à un verrou lecteurs-rédacteurs.
- Définis un sémaphore et utilise-le comme verrouDécris comment la valeur d’un sémaphore change lors des opérations P et V, choisis sa valeur initiale et utilise un sémaphore binaire pour assurer l’exclusion mutuelle.
- Ordonne les fils d’exécution et limite la concurrence avec des sémaphoresUtilise un sémaphore initialisé à zéro pour faire attendre un fil d’exécution par un autre, et un sémaphore à compteur pour limiter le nombre de fils qui utilisent simultanément une ressource.
- Construis un tampon borné avec des sémaphoresRésous le problème des producteurs et des consommateurs avec deux sémaphores à compteur et un mutex, et explique pourquoi leur ordre d’acquisition détermine si le tampon fonctionne ou provoque un interblocage.
- Partage des données avec un verrou lecteurs-rédacteursAutorise plusieurs lecteurs ou un seul rédacteur à la fois, construis un tel verrou à partir de sémaphores et explique son problème de famine et ses coûts.
- Définis un sémaphore et utilise-le comme verrou
- Identifie les violations d’atomicité et les violations d’ordre, les deux types de problèmes à l’origine de presque tous les bogues de concurrence sans interblocage relevés dans une étude de logiciels réels, et corrige chacun avec l’outil adapté.
- Repère et corrige les violations d’atomicitéReconnais le code qui suppose qu’un groupe d’accès s’exécute sans interruption, montre l’entrelacement qui le met en échec et protège tout le groupe avec un seul verrou.
- Repère et corrige les violations d’ordreReconnais le code qui suppose qu’un accès a lieu avant un autre et impose cet ordre avec une variable de condition, un sémaphore ou une jonction, sans dépendre du temps d’exécution.
- Repère et corrige les violations d’atomicité
- Explique comment un interblocage résulte de quatre conditions, préviens-le en concevant le système de manière à supprimer l’une d’elles et décris comment les systèmes évitent les interblocages grâce à des informations connues à l’avance, ou les détectent et rétablissent leur fonctionnement.
- Explique comment un interblocage se produitDéfinis l’interblocage, montre l’entrelacement qui le provoque, nomme les quatre conditions nécessaires et utilise un graphe d’attente pour déterminer si des fils d’exécution sont en interblocage.
- Préviens l’interblocage en supprimant une de ses conditions nécessairesConçois le code de sorte que l’une des quatre conditions ne puisse jamais être réunie : impose un ordre d’acquisition des verrous, prends tous les verrous simultanément, libère-les et réessaie avec une tentative de verrouillage non bloquante, ou évite les verrous grâce aux instructions atomiques.
- Évite l’interblocage grâce à l’ordonnancement et à l’algorithme du banquierUtilise la connaissance préalable de ce que les fils d’exécution peuvent demander pour n’accorder que les demandes qui maintiennent le système dans un état sûr, en appliquant l’algorithme du banquier à des valeurs concrètes.
- Détecte les interblocages et rétablis le fonctionnementLaisse les interblocages se produire, trouve-les en recherchant des cycles, rétablis le fonctionnement en terminant un fil d’exécution ou en le ramenant à un état antérieur, et compare cette approche à la prévention, à l’évitement et à l’absence d’intervention.
- Explique comment un interblocage se produit
- Structure un programme autour d’une boucle d’événements qui réagit aux entrées et sorties disponibles, évite les blocages grâce aux entrées-sorties asynchrones et à la sauvegarde de l’état, et compare les événements aux fils d’exécution.
- Exécute une boucle d’événements avec select ou pollÉcris une boucle d’événements à un seul fil d’exécution qui demande au système d’exploitation quels descripteurs sont disponibles, traite chaque événement jusqu’au bout et ne se bloque jamais.
- Évite les blocages grâce aux entrées-sorties asynchrones et à la sauvegarde de l’étatLance une entrée-sortie sans attendre sa fin, détermine quand elle est terminée et conserve pendant cet intervalle l’état dont un gestionnaire a besoin.
- Choisis entre événements et fils d’exécutionCompare les conceptions fondées sur les événements et sur les fils d’exécution selon le contrôle, la mémoire, l’utilisation de plusieurs cœurs et la maintenabilité, et reconnais les bogues qui subsistent dans le code événementiel.
- Exécute une boucle d’événements avec select ou poll
Sémaphores et interblocage : cours
1Attendre un changement de l’état partagé
Un fil d’exécution a souvent besoin de plus qu’un accès exclusif : il a besoin qu’un autre fil termine son travail ou rende des données disponibles. Les variables de condition lui permettent de se mettre en sommeil pendant cette attente. Leur bon fonctionnement repose sur une variable d’état partagée, un mutex et une transition soigneusement organisée entre la vérification et la mise en sommeil. Commence par un fil père qui attend un fil fils, puis utilise le même schéma pour coordonner les producteurs et les consommateurs sans gaspiller de temps processeur ni perdre de réveils.
1.1 Un état, un mutex et un endroit où se mettre en sommeil
Un mutex répond à la question « Qui peut accéder à ces données maintenant ? ». Il ne répond pas à la question « Le travail est-il terminé ? ». Pour attendre un événement, un fil d’exécution a besoin d’un enregistrement de cet événement et d’un moyen de se mettre en sommeil jusqu’à ce que cet enregistrement change.
Pourquoi l’attente active est une mauvaise méthode
La boucle while (done == 0) ; consomme du temps processeur en vérifiant un indicateur à répétition. Sur un seul processeur, ce travail retarde le fil fils qui doit modifier l’indicateur. En C, la lecture non synchronisée d’une variable ordinaire pendant qu’un autre fil y écrit constitue aussi une course aux données. Le compilateur n’est pas obligé de recharger l’indicateur à chaque itération.
Une variable de condition fournit une file d’attente aux fils d’exécution. POSIX propose pthread_cond_t, initialisé avec PTHREAD_COND_INITIALIZER ou pthread_cond_init(). La file est distincte de l’état testé : done enregistre la fin du travail ; la variable de condition fournit un endroit où se mettre en sommeil.
Garde la vérification et l’attente ensemble
pthread_cond_wait(&c, &m) exige que le fil appelant détienne le mutex m. L’appel libère ce mutex et commence l’attente de façon atomique. Avant de revenir, il reprend le mutex. Cela ferme l’intervalle dangereux dans lequel un autre fil d’exécution pourrait modifier l’état et signaler avant que le fil en attente ait effectivement commencé à attendre.
Voici un fil père qui attend la fin d’un fil fils. Les noms lock, wait, signal et unlock abrègent les appels correspondants de la bibliothèque de fils d’exécution.
/* Parent */
lock(&m);
while (done == 0)
wait(&c, &m);
unlock(&m);
/* Child */
lock(&m);
done = 1;
signal(&c);
unlock(&m);
Au départ, done vaut 0. Si le père s’exécute en premier, il prend le verrou, constate que le travail n’est pas terminé et attend. L’attente libère m, ce qui permet au fils de le prendre, de modifier done et de signaler. Le père reprend finalement m, vérifie de nouveau done et continue.
Si le fils s’exécute en premier, il enregistre la fin du travail avant l’arrivée du père. Son signalement ne réveille personne, mais le père constate ensuite que done == 1 et n’attend pas. Le fonctionnement doit être correct dans les deux ordres.
Les signalements ne mémorisent pas les événements
pthread_cond_signal(&c) débloque au moins un fil d’exécution lorsqu’il y en a en attente ; POSIX autorise le réveil de plusieurs fils. Si aucun fil n’attend, l’appel n’a aucun effet. pthread_cond_broadcast(&c) réveille tous les fils actuellement en attente. Aucun des deux appels ne conserve une notification pour les fils futurs.
Omettre done compromet donc le schéma de jonction : un fils peut signaler avant que son père n’attende, puis le père se met en sommeil sans qu’un autre signalement ne soit prévu. C’est un réveil perdu. Vérifier done en dehors du mutex crée une autre possibilité de réveil perdu : le père constate que la condition est fausse, le fils la rend vraie et signale, puis le père commence à attendre.
Protège la vérification de l’état et chaque modification de l’état avec le même mutex. Détiens-le depuis la vérification jusqu’à l’appel d’attente. Une méthode simple consiste à modifier l’état et à signaler tout en détenant encore ce mutex. POSIX autorise le signalement sans le détenir, mais le protocole d’accès à l’état partagé doit tout de même empêcher les réveils perdus.
Points clés
- L’état enregistre l’événement ; la variable de condition fournit une file d’attente.
- Appelle l’opération d’attente en détenant le mutex ; elle le libère de façon atomique et le reprend avant de revenir.
- Protège la vérification et la modification de l’état avec le même mutex.
- Un signalement envoyé lorsqu’aucun fil d’exécution n’attend n’est pas conservé.
1.2 Vérifier de nouveau après le réveil
Le retour de l’attente signifie que le fil d’exécution peut réessayer. Il ne signifie pas que la condition dont il a besoin est encore vraie. Cette distinction compte lorsque plusieurs fils sont en concurrence pour les mêmes données.
Sémantiques de Mesa et de Hoare
| Sémantique | Ce qu’un signalement apporte au fil en attente |
|---|---|
| Mesa | L’état prêt ; le fil en attente doit reprendre le mutex et vérifier de nouveau l’état. |
| Hoare | La transmission immédiate du verrou et de l’exécution, qui préserve la condition signalée au moment de cette transmission. |
POSIX, Java, les variables de condition de C++ et les conditions de Python suivent le comportement de Mesa. Les moniteurs de Hoare ont introduit le modèle de transmission plus fort ; l’approche de Mesa est le choix courant en pratique.
Supposons que C1 attende qu’un tampon d’une place ne soit plus vide. Un producteur le remplit et signale C1. C1 est désormais prêt, mais C2 peut acquérir le mutex en premier et prendre l’élément. Lorsque C1 reprend enfin le mutex, le tampon est de nouveau vide.
lock(&m);
while (count == 0)
wait(&filled, &m);
v = get();
unlock(&m);
Le while répète le test après chaque retour. Un if ne teste qu’avant la première attente et laisserait C1 appeler get() sur un tampon vide. Même avec un producteur et un consommateur, utilise while : POSIX autorise aussi les réveils intempestifs, où l’attente se termine sans signalement correspondant.
Réveiller un fil d’exécution ou tous ?
Un signalement convient lorsque les fils en attente ont besoin de la même condition et que n’importe lequel peut profiter du changement. Une diffusion est utile lorsque plusieurs peuvent continuer, ou lorsque différents fils en attente ont besoin de quantités différentes et que le fil qui signale ne peut pas choisir le bon.
Considérons un allocateur disposant de 0 octet libre. Ta a besoin de 100 octets et Tb de 10. Un autre fil d’exécution libère 50 octets. Un signalement peut réveiller Ta, qui ne peut toujours pas continuer, laissant Tb endormi alors que sa demande peut être satisfaite. Une diffusion réveille les deux. Ils reprennent le mutex l’un après l’autre ; Tb peut allouer la mémoire, tandis que Ta attend à nouveau. C’est une condition couvrante : la notification couvre chaque fil en attente susceptible de pouvoir continuer.
La diffusion peut provoquer des réveils et des commutations de contexte supplémentaires, ainsi qu’une concurrence pour le mutex. Des variables de condition distinctes pour des prédicats distincts peuvent rendre le signalement plus sélectif. Dans un tampon, les producteurs et les consommateurs ne devraient pas partager une seule file de signalement indifférenciée.
Le lien avec les moniteurs
Un moniteur regroupe des données partagées avec des opérations qui admettent un seul fil d’exécution à la fois et des variables de condition pour attendre. Les objets Java fournissent un verrou et un ensemble de fils en attente. Dans synchronized, un fil peut appeler wait(), notify() ou notifyAll() ; les appeler sans détenir le moniteur de cet objet déclenche IllegalMonitorStateException.
L’attente ne libère que le mutex associé ou le verrou de l’objet Java. Si un fil d’exécution détient aussi un autre verrou, ce second verrou reste détenu. Un producteur qui en a besoin peut ne pas être en mesure de rendre la condition vraie. Vérifie tous les verrous détenus au moment d’une attente, pas seulement celui qui est nommé dans l’appel.
Points clés
- Selon la sémantique de Mesa, le réveil rend un fil d’exécution prêt ; il doit encore entrer en concurrence pour obtenir le mutex.
- Attends dans une boucle while pour gérer les changements d’état et les réveils intempestifs.
- Utilise une diffusion lorsqu’un seul signalement pourrait laisser endormi un fil en attente qui peut continuer.
- Des conditions d’attente distinctes peuvent utiliser des variables de condition distinctes.
- L’attente ne libère que le verrou qui lui est associé.
1.3 Construire un tampon borné avec deux files d’attente
Les producteurs ajoutent des éléments à un tampon de taille fixe ; les consommateurs les retirent. Un tampon borné correct empêche d’écraser un tampon plein, de retirer un élément d’un tampon vide et de modifier simultanément ses données de gestion. La file de requêtes d’un serveur Web et le tube entre grep error log.txt et wc -l suivent tous deux ce schéma.
Suivre les indices du tampon circulaire
Un tampon circulaire utilise un tableau et deux indices. fill_ptr indique la prochaine écriture ; use_ptr indique la prochaine lecture. Les deux avancent modulo MAX. Un compteur count distinct permet de distinguer un tampon vide d’un tampon plein, même lorsque les deux indices sont égaux.
void put(int v) {
buffer[fill_ptr] = v;
fill_ptr = (fill_ptr + 1) % MAX;
count++;
}
int get(void) {
int v = buffer[use_ptr];
use_ptr = (use_ptr + 1) % MAX;
count--;
return v;
}
Pars d’une capacité de 3 avec les deux indices à 0. L’ajout de 7, 8 et 9 remplit les places 0, 1 et 2. L’indice d’écriture revient à 0 et count atteint 3. La lecture renvoie 7 et avance l’indice de lecture à 1. L’ajout de 10 réutilise la place 0. Le tableau est maintenant [10, 8, 9], mais le retrait premier entré, premier sorti (FIFO) suit l’indice de lecture : 8 vient ensuite, puis 9, puis 10.
Associe chaque signalement au rôle opposé
Utilise un mutex, empty pour les producteurs qui attendent une place et fill pour les consommateurs qui attendent un élément. Initialise les deux variables de condition et le mutex avant de démarrer les fils d’exécution.
/* Producer: repeat for each item v */
lock(&m);
while (count == MAX)
wait(&empty, &m);
put(v);
signal(&fill);
unlock(&m);
/* Consumer */
lock(&m);
while (count == 0)
wait(&fill, &m);
v = get();
signal(&empty);
unlock(&m);
La vérification et la mise à jour du tampon restent dans une seule section critique. Sinon, deux producteurs pourraient tous deux voir une place libre et écraser le travail de l’autre. L’attente libère m, ce qui permet au fil du rôle opposé de modifier le tampon.
Pourquoi une seule file peut bloquer la progression
Avec une seule variable de condition et des signalements, les producteurs et les consommateurs peuvent réveiller un fil du mauvais rôle. Pour une capacité de 1, C1 et C2 se mettent d’abord en sommeil parce que le tampon est vide. P insère un élément, réveille C1, puis attend parce que le tampon est plein. C1 retire l’élément et signale, mais le signalement peut réveiller C2 au lieu de P. C1 attend à nouveau ; C2 trouve aussi le tampon vide et attend. P reste endormi alors qu’une place est disponible. Deux files dirigent chaque signalement vers le rôle qui peut désormais continuer. Une diffusion sur une file partagée peut éviter ce problème de sélection, au prix de réveils supplémentaires.
Une capacité plus grande absorbe les pointes d’activité et permet de recouvrir les travaux de production et de consommation effectués en dehors de la courte section critique du tampon. Les mises à jour du tampon lui-même restent sérialisées. Une capacité de 1 oblige chaque insertion et chaque retrait à alterner.
Pour un arrêt propre, attends d’abord que chaque producteur ait terminé de placer ses éléments réels. Place ensuite dans la file un élément réservé de fin de données par consommateur, comme -1 lorsque les éléments réels sont positifs ou nuls. Chaque consommateur quitte sa boucle après en avoir pris un. Envoyer ces marqueurs alors que des producteurs sont encore actifs peut laisser des éléments réels sans consommateur.
Points clés
- Avance les indices du tampon circulaire modulo sa capacité ; le compteur distingue le tampon vide du tampon plein.
- Les producteurs attendent une place et signalent les éléments ; les consommateurs attendent des éléments et signalent les places.
- Utilise des boucles while et un mutex autour du prédicat et de la mise à jour du tampon.
- Une file de signalement partagée peut réveiller un fil du mauvais rôle et laisser tous les fils d’exécution endormis.
- Attends la fin de tous les producteurs avant de placer un marqueur de fin par consommateur dans la file.
- L’ordre FIFO suit l’indice de lecture, et non l’ordre physique du tableau.
2Compter les jetons avec des sémaphores
Un sémaphore enregistre des jetons que les fils d’exécution acquièrent et rendent. Sa valeur initiale permet à la même primitive d’imposer un accès exclusif, de mémoriser un événement ou de limiter l’accès à un ensemble de ressources. Suis l’évolution de sa valeur au fil des opérations P et V, puis associe le compte des jetons à un mutex pour construire un tampon borné. Une conception lecteurs-rédacteurs ajoute un autre usage : partager l’accès entre lecteurs tout en assurant l’exclusivité aux rédacteurs, avec un coût à examiner en matière d’équité.
2.1 Choisir la valeur initiale d’un sémaphore
Un sémaphore contient un entier modifié par les opérations atomiques P et V. Dijkstra l’a introduit dans les années 1960 sous les noms P et V ; POSIX nomme les opérations sem_wait() et sem_post(). Atomique signifie que deux fils d’exécution ne peuvent pas effectuer partiellement la même opération sur le compteur au même moment.
Les unités disponibles déterminent la valeur initiale
sem_init(&s, 0, X) crée un sémaphore sans nom de valeur X. Le deuxième argument, 0, sélectionne le partage entre les fils d’exécution d’un même processus. Pour un partage entre processus, un argument non nul impose que le sémaphore se trouve en mémoire partagée.
| Usage | Valeur initiale | Signification |
|---|---|---|
| Verrou | 1 | Un fil d’exécution peut entrer immédiatement. |
| Ordre des événements | 0 | L’événement ne s’est pas produit. |
| Ensemble de ressources | N | N unités peuvent être acquises immédiatement. |
Un sémaphore binaire utilisé comme verrou encadre une section critique par une opération P et une opération V. L’initialiser à 0 bloque le premier fil qui veut entrer. L’initialiser à 2 autorise l’entrée de deux fils et ne fournit pas l’exclusion mutuelle.
Deux modèles de comptage équivalents
Dans la définition de Dijkstra, la valeur reste positive ou nulle : l’opération P met le fil d’exécution en sommeil jusqu’à ce qu’elle puisse décrémenter sans descendre sous zéro, et l’opération V ajoute une unité. Linux suit cette convention de valeur positive ou nulle.
Pour les traces à la main, utilise le modèle à compteur signé : l’opération P décrémente et bloque si la nouvelle valeur est négative ; l’opération V incrémente et réveille un fil en attente lorsqu’il y en a un de bloqué. Dans ce modèle, la valeur absolue d’une valeur négative donne le nombre de fils bloqués en attente. C’est une convention de raisonnement, et non une valeur négative à attendre de l’API des sémaphores de Linux.
Suis un verrou initialisé à 1 :
- T0 effectue une opération P : 1 devient 0, donc T0 entre.
- T1 effectue une opération P : 0 devient −1, donc T1 se bloque.
- T0 effectue une opération V : −1 devient 0, ce qui permet à T1 de continuer.
- T1 termine et effectue une opération V : 0 devient 1.
Le réveil transfère le jeton existant à T1 ; une autre opération P n’est pas nécessaire avant que T1 entre dans sa section critique.
Une opération V est mémorisée
Sans fil d’exécution en attente, une opération V augmente le nombre de jetons disponibles. Une opération P ultérieure peut consommer ce jeton. Cela est utile lorsqu’un fil termine avant que le fil qui l’attend ait commencé. Le signalement d’une variable de condition ne possède pas une telle mémoire.
Les sémaphores n’ont pas de propriétaire : un fil d’exécution peut effectuer une opération P et un autre une opération V. Un mutex appartient au fil qui l’a verrouillé ; le déverrouillage d’un mutex POSIX par défaut par un autre fil produit un comportement indéfini. Le sémaphore sans propriétaire permet le signalement, mais rend aussi une opération V supplémentaire plus difficile à diagnostiquer.
Un sémaphore peut être construit à partir d’un mutex, d’une variable de condition et d’un entier positif ou nul. L’opération P prend le verrou, boucle tant que value <= 0, attend, décrémente et libère le verrou. L’opération V prend le verrou, incrémente, signale et libère le verrou. Le mutex rend les opérations sur le compteur atomiques ; la boucle gère les réveils qui ne laissent pas d’unité disponible. Les verrous avec variables de condition et les sémaphores peuvent être construits les uns à partir des autres, même si une implémentation correcte des variables de condition à partir de sémaphores est plus complexe.
Points clés
- Initialise un sémaphore au nombre d’unités disponibles immédiatement.
- Un sémaphore utilisé comme verrou commence à 1 et associe une opération P avant la section à une opération V après celle-ci.
- Le modèle à compteur signé utilise des valeurs négatives pour les fils d’exécution bloqués en attente ; les valeurs de Linux restent positives ou nulles.
- Les opérations V sont mémorisées sous forme de jetons, même si aucun fil d’exécution n’attend.
- Un sémaphore n’a pas de propriétaire ; un mutex en a un.
2.2 Mémoriser les événements et limiter l’utilisation des ressources
La même paire d’opérations P et V peut représenter un événement ou une quantité limitée de ressources. Décide de ce que représente un jeton avant de choisir le compte initial et l’emplacement de chaque opération.
Faire précéder l’utilisation par la fin du travail
Pour qu’un fil père attende un fil fils, initialise finished à 0. Le fils effectue une opération V après avoir terminé son travail. Le père effectue une opération P avant d’utiliser le résultat.
/* Child */
produce_result();
sem_post(&finished);
/* Parent */
sem_wait(&finished);
use_result();
Si le père arrive en premier, la valeur signée passe de 0 à −1 et il se met en sommeil. L’opération V du fils ramène la valeur à 0 et le réveille. Si le fils arrive en premier, son opération V fait passer la valeur de 0 à 1 ; l’opération P ultérieure fait passer la valeur de 1 à 0 sans blocage. Un événement achevé permet une opération P réussie.
Compter les ressources disponibles
Un ensemble de quatre connexions à une base de données utilise un sémaphore initialisé à 4. Chaque client effectue une opération P avant d’emprunter une connexion et une opération V après l’avoir rendue. Un cinquième emprunteur simultané se met en sommeil. Le sémaphore compte les admissions ; un mutex distinct protège toujours la liste partagée qui indique quelle connexion est libre. Deux fils d’exécution admis ne doivent pas retirer la même entrée de la liste.
La limitation de la concurrence applique la même idée à une phase coûteuse. Supposons que 100 fils d’exécution aient chacun besoin de 500 Mo pendant le traitement. Ensemble, ils pourraient avoir besoin de 50 000 Mo. Une machine de 8 Go dispose de 8 000 Mo selon les unités décimales du SI. Admettre au plus 12 travailleurs limite cette phase à 6 000 Mo et laisse 2 000 Mo pour d’autres usages.
sem_wait(&processing_slots);
run_memory_heavy_phase();
sem_post(&processing_slots);
Chaque chemin après une opération P réussie doit rendre son jeton, y compris un chemin de traitement d’erreur. Oublier une opération V réduit progressivement la capacité. Effectuer deux opérations V crée un jeton supplémentaire et autorise plus d’utilisateurs simultanés que prévu.
Se retrouver à un rendez-vous
Un rendez-vous exige l’arrivée de deux fils d’exécution avant que l’un ou l’autre ne continue. Initialise a_arrived et b_arrived à 0.
/* A */
sem_post(&a_arrived);
sem_wait(&b_arrived);
/* B */
sem_post(&b_arrived);
sem_wait(&a_arrived);
Si A arrive en premier, il enregistre son arrivée et se bloque en attendant B. B enregistre son arrivée et peut consommer le jeton conservé pour A. A peut ensuite consommer le jeton de B. Inverser les deux séquences pour effectuer l’opération P avant l’opération V laisse chaque fil endormi avant qu’il n’annonce quoi que ce soit.
Ces deux sémaphores réalisent un rendez-vous unique. Pour réutiliser la synchronisation sur plusieurs cycles, il faut empêcher que les arrivées d’un cycle soient consommées par un autre ; une trace réussie ne constitue pas une conception de barrière réutilisable.
Un sémaphore convient bien à un compte ou à un événement simple. Une variable de condition convient à un prédicat plus riche, par exemple attendre qu’une file contienne plusieurs éléments d’un type donné. Dans ce cas, le mutex protège l’état à partir duquel la condition est calculée.
Points clés
- Initialise un sémaphore d’ordonnancement à 0 et n’effectue l’opération V qu’après l’événement qu’il représente.
- Un sémaphore à compteur limite les admissions mais ne protège pas la liste des ressources.
- La limitation de la concurrence borne la demande simultanée de mémoire ou d’une autre ressource limitée.
- Pour un rendez-vous entre deux fils d’exécution, annonce l’arrivée avant d’attendre l’autre fil.
- Une opération V supplémentaire crée de la capacité ; une opération V manquante en supprime.
2.3 Réserver une place avant de verrouiller le tampon
Un tampon utilisant des sémaphores sépare deux questions : « Un élément ou une place est-il disponible ? » et « Qui peut mettre à jour le tableau maintenant ? » Deux sémaphores à compteur répondent à la première question et un mutex répond à la seconde.
Mettre en place trois mécanismes distincts
Pour une capacité MAX, initialise empty à MAX, full à 0 et le mutex à l’état déverrouillé. Ici, empty et full sont des sémaphores, et non des variables de condition. Un jeton de empty réserve une place ; un jeton de full réserve un élément.
/* Producer */
sem_wait(&empty);
lock(&mutex);
put(v);
unlock(&mutex);
sem_post(&full);
/* Consumer */
sem_wait(&full);
lock(&mutex);
v = get();
unlock(&mutex);
sem_post(&empty);
Le producteur acquiert une place avant de toucher au tampon circulaire, puis rend l’élément inséré disponible en effectuant une opération V sur full. Le consommateur acquiert un élément, le retire sous la protection du mutex et rend une place avec une opération V sur empty. Une opération P réussie sur un sémaphore réserve déjà la ressource : ces appels n’ont donc pas besoin d’être entourés d’une boucle de vérification du prédicat comme avec une variable de condition.
Suivre le cas d’une place
Au départ, empty vaut 1 et full vaut 0. Un consommateur effectue une opération P sur full, dont la valeur passe à −1 dans le modèle signé, et se met en sommeil. Le producteur effectue une opération P sur empty, dont la valeur passe de 1 à 0. Il prend le verrou, insère l’élément, libère le verrou et effectue une opération V sur full, dont la valeur passe de −1 à 0, ce qui débloque le consommateur.
L’opération P suivante du producteur sur empty fait passer sa valeur de 0 à −1 et le bloque. Le consommateur prend l’élément sous la protection du mutex et effectue une opération V sur empty, dont la valeur passe de −1 à 0, ce qui débloque le producteur. Les compteurs des sémaphores assurent le passage de relais ; aucun count distinct n’est nécessaire pour la synchronisation.
Avec un tampon plus grand, plusieurs producteurs peuvent franchir le sémaphore des places libres. Sans le mutex, deux d’entre eux pourraient lire le même fill_ptr et écrire dans la même place. Une réservation ne donne pas un accès exclusif aux indices du tableau.
Pourquoi prendre le verrou d’abord peut provoquer un interblocage
Supposons que le consommateur verrouille mutex avant d’effectuer une opération P sur full. Si le tampon est vide, il se met en sommeil tout en détenant le mutex. Le producteur a besoin de ce mutex pour insérer l’élément dont le signalement débloquerait le consommateur. Aucun des deux ne peut terminer.
Une opération P sur un sémaphore ne libère aucun mutex. Une attente sur une variable de condition libère délibérément le mutex qui lui est associé. L’ordre dans ce tampon compte donc : réserve d’abord, prends le verrou ensuite, modifie le tampon, libère le verrou, puis rends disponible le jeton correspondant au rôle opposé.
La règle plus générale consiste à éviter de se bloquer en détenant un verrou nécessaire à l’opération qui peut te débloquer. Cette règle explique pourquoi l’algorithme lecteurs-rédacteurs peut utiliser une organisation différente sans danger : son rédacteur n’a pas besoin du verrou du compteur de lecteurs pour libérer sa ressource.
Le mutex peut lui-même être un sémaphore binaire initialisé à 1. Son rôle reste l’exclusion mutuelle ; les deux autres sémaphores comptent les ressources.
Points clés
- Initialise empty à la capacité, full à 0 et le mutex à l’état déverrouillé.
- Réserve une place ou un élément avant d’acquérir le mutex du tampon.
- Protège les indices du tampon circulaire et les mises à jour du tableau même lorsque les jetons sont correctement comptés.
- Une opération P sur un sémaphore ne libère aucun mutex.
- Effectue une opération V sur full après une insertion et sur empty après un retrait.
- Un fil d’exécution bloqué ne doit pas détenir un verrou dont le fil qui le débloque a besoin.
2.4 Laisser les lecteurs partager l’accès et les rédacteurs travailler seuls
Les structures qui font surtout l’objet de lectures peuvent admettre plusieurs lecteurs simultanément, car la lecture ne modifie pas les données. Un verrou lecteurs-rédacteurs admet soit un groupe de lecteurs, soit un seul rédacteur. Toute modification des données protégées exige un accès en écriture.
Former le groupe de lecteurs
Utilise readers = 0, un sémaphore lock initialisé à 1 pour protéger ce compteur, et writelock initialisé à 1 pour protéger la ressource partagée.
/* Acquire read access */
sem_wait(&lock);
readers++;
if (readers == 1)
sem_wait(&writelock);
sem_post(&lock);
/* Release read access */
sem_wait(&lock);
readers--;
if (readers == 0)
sem_post(&writelock);
sem_post(&lock);
Un rédacteur effectue une opération P sur writelock, modifie les données, puis effectue une opération V sur writelock. Il n’acquiert pas le verrou du compteur. Le premier lecteur prend writelock pour le groupe et le dernier lecteur le rend. Un sémaphore convient à ce passage de relais, car le fil d’exécution qui effectue l’opération V n’a pas besoin d’être celui qui a effectué l’opération P initiale.
Suis l’entrée de R1 : il porte readers à 1 et prend writelock. R2 porte le compteur à 2 sans prendre un autre jeton. Le rédacteur W se bloque alors sur writelock. R1 part et ramène le compteur à 1. R2 part, le ramène à 0 et effectue une opération V, ce qui permet à W d’écrire seul.
Le premier lecteur peut attendre writelock tout en détenant lock. Cela ne crée pas l’interblocage du tampon borné : le rédacteur actuel n’a pas besoin du verrou du compteur pour terminer et effectuer une opération V sur writelock.
L’équité est une propriété distincte
Cette conception donne la priorité aux lecteurs. Tant que le compteur de lecteurs reste positif, un autre lecteur peut rejoindre le groupe avant un rédacteur en attente. Un flux continu de lecteurs dont les accès se chevauchent peut faire attendre le rédacteur indéfiniment : c’est la famine des rédacteurs.
| Politique | Choix d’admission | Risque |
|---|---|---|
| Priorité aux lecteurs | De nouveaux lecteurs peuvent rejoindre les lecteurs actifs. | Les rédacteurs peuvent subir une famine. |
| Priorité aux rédacteurs | Les rédacteurs en attente empêchent l’entrée de nouveaux lecteurs. | Les lecteurs peuvent subir une famine. |
| Admission équitable | Les arrivants obtiennent l’accès dans un ordre équitable. | Plus de gestion des admissions. |
La conception de base assure la sûreté, mais ne borne pas la durée d’attente d’un rédacteur. Un débit de lecteurs plus élevé ne garantit pas un service équitable.
Utiliser le bon mode
Un verrou de lecture ne peut pas protéger une écriture contre les autres lecteurs. Même la mise à jour d’un compteur de cache avec un accès en lecture peut entrer en concurrence avec un autre lecteur. Pour effectuer une conversion dans cette conception de base, libère l’accès en lecture, acquiers l’accès en écriture et vérifie de nouveau la condition. Attendre un accès en écriture tout en étant encore compté comme lecteur peut amener le fil d’exécution à s’attendre lui-même indéfiniment.
POSIX fournit pthread_rwlock_t, pthread_rwlock_rdlock(), pthread_rwlock_wrlock() et pthread_rwlock_unlock(). Java propose ReentrantReadWriteLock ; C++17 propose std::shared_mutex. Ces verrous nécessitent plus de gestion qu’un simple mutex. Mesure la charge de travail : des sections courtes ou des écritures fréquentes peuvent rendre un simple mutex aussi rapide, voire plus rapide.
Points clés
- Un verrou lecteurs-rédacteurs autorise plusieurs lecteurs ou un seul rédacteur en accès exclusif.
- Le premier lecteur acquiert le jeton du groupe ; le dernier lecteur le rend.
- Le compteur de lecteurs a besoin de son propre verrou.
- La priorité aux lecteurs peut provoquer la famine d’un rédacteur en attente.
- Libère l’accès en lecture avant de demander l’accès en écriture, puis vérifie de nouveau les données.
3Trouver l’hypothèse d’ordre à l’origine d’un bogue
De nombreuses défaillances dues à la concurrence laissent le programme continuer avec des données incorrectes. Deux causes récurrentes consistent à considérer plusieurs accès comme une seule action indivisible et à supposer que le travail d’un fil d’exécution précède celui d’un autre. Le suivi de quelques accès soigneusement choisis révèle souvent l’hypothèse cachée. La correction dépend de cette hypothèse : protège une opération entière lorsqu’elle doit rester indivisible et établis une dépendance explicite lorsqu’une opération doit se produire en premier.
3.1 Protéger l’ensemble de la vérification et de l’action
Une violation d’atomicité se produit lorsque le code suppose qu’un groupe d’accès est indivisible, mais qu’un autre fil d’exécution peut agir entre ces accès. Chaque accès pris séparément peut sembler raisonnable alors que l’opération dans son ensemble échoue.
Repérer l’intervalle après la vérification
Considérons un tampon de journal partagé :
/* T1 */
if (log->buf != NULL)
write_out(log->buf);
/* T2 */
log->buf = NULL;
T1 peut vérifier qu’un pointeur n’est pas nul, s’interrompre et reprendre après que T2 l’a mis à zéro. T1 transmet alors un pointeur nul à write_out(). La garantie voulue ne se limitait pas à une vérification valide : la valeur vérifiée devait rester utilisable jusqu’à l’appel.
Protège ensemble la vérification et l’utilisation, et protège l’affectation concurrente avec le même mutex :
/* T1 */
lock(&m);
if (log->buf != NULL)
write_out(log->buf);
unlock(&m);
/* T2 */
lock(&m);
log->buf = NULL;
unlock(&m);
T2 s’exécute désormais soit avant, soit après l’opération entière de T1. Donner à chaque fil d’exécution son propre verrou ne résout rien : les deux pourraient détenir leurs verrous distincts simultanément. Ne verrouiller que T1 laisse T2 libre d’intervenir dans l’intervalle.
Un test valide peut tout de même mener à un solde invalide
Pars d’un solde de 50. Deux fils d’exécution comptent chacun retirer 30 uniquement si le solde est suffisant. Les deux peuvent lire 50 et réussir le test balance >= 30 avant que l’un d’eux n’agisse. Si les retraits sont ensuite effectués, le solde peut devenir égal à −10. Protéger ensemble le test et la soustraction permet au premier retrait de laisser 20 ; le fil suivant voit 20 et refuse le retrait.
Même des lectures et des soustractions atomiques ne font pas de cette paire une seule transaction. Un autre fil d’exécution peut agir entre la vérification atomique et la modification atomique. Un verrou autour de toute la décision et de la modification impose la délimitation nécessaire.
Le même problème apparaît dans count++ : la lecture, l’addition et l’écriture sont des étapes distinctes. Deux fils d’exécution peuvent lire la même ancienne valeur et écrire la même valeur incrémentée, ce qui entraîne la perte d’une mise à jour. Un verrou autour de l’incrément ou une opération atomique de lecture et addition effectue la mise à jour requise en toute sécurité.
Utiliser les résultats sans généraliser abusivement
Lu et ses collègues ont examiné 105 bogues de concurrence corrigés dans MySQL, Apache, Mozilla et OpenOffice dans une étude de 2008. Parmi eux, 74 étaient des bogues sans interblocage et 31 étaient des interblocages. Les violations d’atomicité et d’ordre représentaient 97 % de l’échantillon de bogues sans interblocage. Ces résultats concernent l’échantillon étudié ; ils ne garantissent pas ce pourcentage pour toute base de code.
Environ 92 % des bogues étudiés pouvaient être déclenchés en contrôlant l’ordre de quatre accès mémoire au plus. Un petit tableau d’entrelacement peut donc être plus révélateur que des milliers d’exécutions ordinaires réussies. ThreadSanitizer aide à trouver les courses aux données qui se produisent pendant l’exécution, mais un bogue logique d’atomicité peut subsister même lorsque chaque accès individuel est synchronisé. Teste l’invariant de l’opération entière, par exemple : « un retrait réussi ne rend jamais le solde négatif ».
Points clés
- Délimite la section critique autour de toute l’opération qui doit être indivisible.
- Chaque accès concurrent doit suivre le même protocole de verrouillage.
- Des accès individuellement atomiques peuvent tout de même laisser un intervalle entre la vérification et l’action.
- Suis un petit entrelacement problématique au lieu de te fier uniquement à des exécutions réussies répétées.
3.2 Faire en sorte qu’une opération ait lieu avant une autre
Une violation d’ordre signifie que l’opération A doit avoir lieu avant l’opération B, mais que le programme ne fournit aucune synchronisation qui le garantisse. L’exclusion mutuelle seule ne peut pas choisir quelle opération passe en premier.
Un fil d’exécution peut s’exécuter immédiatement
Supposons qu’un fil créateur enregistre dans un pointeur partagé un nouvel enregistrement de travailleur, tandis que le travailleur commence par utiliser ce pointeur :
/* Creator */
worker = thread_create(worker_main);
/* New worker */
use(worker->state);
Le travailleur peut s’exécuter avant le retour de thread_create() et avant la fin de l’affectation. L’ordre du code source du créateur ne dit rien sur le moment où le nouveau fil d’exécution obtient du temps processeur. Une exécution réussie signifie simplement que le créateur a remporté cette course par hasard.
Signaler la disponibilité après l’initialisation
Ajoute ready = 0, le mutex m et la variable de condition c. Le créateur conserve le mutex pendant qu’il affecte le pointeur et signale que l’enregistrement est prêt :
/* Creator */
lock(&m);
worker = thread_create(worker_main);
ready = 1;
signal(&c);
unlock(&m);
/* Worker, before using worker */
lock(&m);
while (ready == 0)
wait(&c, &m);
unlock(&m);
use(worker->state);
Si le travailleur atteint le mutex en premier, il ne peut pas franchir le test de disponibilité avant que le créateur ait publié l’enregistrement. S’il arrive plus tard, ready mémorise que l’initialisation est terminée. Maintiens l’enregistrement en vie pendant que le travailleur l’utilise et synchronise toute modification ultérieure.
Un sémaphore initialisé à 0 offre un passage de relais plus court : le créateur effectue une opération V après l’affectation du pointeur et le travailleur effectue une opération P avant de l’utiliser. Pour un résultat qui n’existe qu’une fois un fil d’exécution terminé, effectuer sa jonction avant de lire son résultat établit l’ordre nécessaire. De même, la libération d’un objet doit attendre la fin de son utilisation, par exemple grâce à des jonctions ou à un compteur de références correctement synchronisé.
Pourquoi modifier le temps d’exécution ne corrige pas le problème
Ajouter sleep(1) ou sched_yield() ne modifie que la probabilité de l’ordonnancement souhaité. Un système plus lent ou chargé peut toujours laisser le consommateur continuer trop tôt. Un mutex autour de chaque opération empêche leur chevauchement, mais permet toujours à l’utilisation d’acquérir le mutex avant l’initialisation.
| Hypothèse cachée | Type de bogue | Garantie nécessaire |
|---|---|---|
| « Rien ne change entre cette vérification et cette action. » | Violation d’atomicité | Une opération protégée. |
| « L’initialisation se termine avant cette utilisation. » | Violation d’ordre | Une dépendance explicite à la fin ou à la disponibilité. |
Dans l’étude de Lu et de ses collègues, environ un tiers des bogues sans interblocage étaient des violations d’ordre, et environ 73 % des bogues sans interblocage n’ont pas été corrigés par le simple ajout ou la modification de verrous. Avant de choisir une primitive, nomme l’opération qui doit se terminer et celle qui doit attendre. Cette dépendance détermine où placer l’opération V, le signalement ou la jonction.
Points clés
- Une violation d’ordre suppose qu’une action antérieure est terminée sans imposer cet ordre.
- Signale la disponibilité uniquement après l’initialisation et attends avant d’utiliser le résultat.
- Un verrou empêche le chevauchement, mais ne détermine pas qui passe en premier.
- La mise en sommeil et la cession du processeur modifient le rythme d’exécution sans garantir une dépendance.
- Maintiens les objets partagés en vie jusqu’à ce que tous leurs utilisateurs aient terminé.
4Raisonner sur l’interblocage et la progression
L’interblocage transforme un ensemble d’attentes ordinaires en arrêt permanent : chaque fil d’exécution a besoin de quelque chose que détient un autre fil bloqué. Un graphe de dépendances rend ce cycle visible. Les quatre conditions nécessaires suggèrent des moyens de le prévenir ; la déclaration préalable des besoins en ressources permet de l’éviter ; et un détecteur peut trouver un interblocage après sa formation. Compare ces stratégies selon les informations qu’elles nécessitent, la concurrence qu’elles coûtent et le travail que le rétablissement peut faire perdre.
4.1 Suivre le cycle des fils d’exécution en attente
Un ensemble de fils d’exécution est en interblocage lorsque chacun attend quelque chose que seul un autre fil de l’ensemble peut fournir. Aucun ne peut continuer et accomplir l’action qui libère les autres. Le programme peut fonctionner avec de nombreux ordonnancements avant qu’un ordre défavorable ne révèle le cycle.
Construire la défaillance à deux verrous
T1 acquiert L1 puis demande L2. T2 acquiert L2 puis demande L1. Si T1 prend L1, puis T2 prend L2, les deux secondes acquisitions se bloquent. Chaque fil d’exécution détient précisément la ressource dont l’autre a besoin.
Cela peut se cacher dans des fonctions ordinaires. Si transfer(a, b) verrouille a puis b, les appels simultanés transfer(x, y) et transfer(y, x) peuvent acquérir les verrous des comptes dans des ordres opposés. L’encapsulation rend un appel pratique, mais peut masquer les verrous qu’il prend.
Identifier les quatre conditions
| Condition nécessaire | Signification dans l’exemple des verrous |
|---|---|
| Exclusion mutuelle | Un seul fil d’exécution détient un verrou donné. |
| Possession et attente | Chacun détient un verrou en attendant un autre. |
| Absence de préemption | Un verrou détenu n’est pas retiré de force. |
| Attente circulaire | Chacun attend le verrou détenu par le fil suivant. |
Les quatre conditions sont nécessaires. Supprimer l’une d’elles dès la conception prévient l’interblocage lié aux ressources. La présence de verrous exclusifs et d’acquisitions imbriquées ne suffit pas à établir qu’une exécution donnée a atteint un cycle.
Lire un graphe d’attente
Dessine un nœud par fil d’exécution. Dessine T → U lorsque T attend une ressource détenue par U. Avec des ressources à instance unique comme les verrous exclusifs, un cycle indique un interblocage.
Si T1 détient A et attend B, T2 détient B et attend C, et T3 détient C et attend A, le graphe est T1 → T2 → T3 → T1. Les trois sont bloqués. Si T3 demande à la place un verrou libre D, il peut terminer et libérer C, ce qui permet à T2 puis à T1 de continuer. Une chaîne n’est pas un cycle.
Lorsque des types de ressources ont plusieurs instances interchangeables, un cycle seul ne suffit pas : une instance supplémentaire peut devenir disponible. Utilise le nombre de ressources et les demandes non satisfaites pour évaluer ce cas.
Philosophes à table et défaillances apparentées
Cinq philosophes partagent cinq fourchettes disposées en cercle. Chacun a besoin des deux fourchettes voisines. Si tous prennent d’abord leur fourchette de gauche, tous peuvent attendre indéfiniment leur fourchette de droite. Inverser l’ordre d’acquisition d’un philosophe brise le cycle. Limiter l’admission à quatre philosophes empêche aussi la situation où les cinq détiennent une fourchette ; au moins un philosophe admis peut obtenir les deux fourchettes.
La famine signifie qu’un fil d’exécution attend indéfiniment tandis que d’autres progressent. Le verrouillage actif signifie que des fils s’exécutent et réagissent, mais échouent à répétition à progresser. Un ensemble en interblocage est bloqué. Un fil peut même se mettre lui-même en interblocage en verrouillant un mutex ordinaire non récursif qu’il détient déjà. Les sémaphores et les variables de condition peuvent aussi créer des dépendances : un fil en attente qui détient un verrou nécessaire au fil qui le signale peut fermer un cycle sans seconde acquisition explicite de verrou.
Points clés
- L’interblocage est un cycle permanent de dépendances entre fils d’exécution bloqués.
- Les quatre conditions doivent être réunies : exclusion mutuelle, possession et attente, absence de préemption et attente circulaire.
- Pour des ressources à instance unique, un cycle d’attente indique un interblocage.
- Un cycle impliquant des ressources à plusieurs instances exige une analyse supplémentaire.
- La famine, le verrouillage actif et l’interblocage décrivent des défauts de progression différents.
- Les verrouillages masqués dans les fonctions peuvent inverser l’ordre d’acquisition.
4.2 Supprimer une condition qui permet l’interblocage
La prévention modifie la conception de sorte qu’une condition nécessaire à l’interblocage ne puisse pas se produire. L’ordre d’acquisition des verrous est souvent l’approche la plus claire, car il s’attaque directement à la dépendance circulaire.
Donner un ordre à tous les verrous
Attribue des clés uniques aux verrous et acquiers toujours les clés les plus petites avant les plus grandes. Pour les transferts entre comptes, trie les identifiants des comptes avant de les verrouiller. transfer(x, y) et transfer(y, x) acquièrent alors d’abord le même compte. Si les identifiants désignent le même compte, acquiers son verrou une seule fois.
Un cycle exigerait que les clés continuent à augmenter tout autour du cercle et reviennent à la clé de départ, ce qui est impossible. Chaque chemin d’acquisition doit suivre la règle. Une seule fonction auxiliaire qui inverse l’ordre rétablit le risque. L’outil lockdep de Linux suit les dépendances de verrouillage observées et signale les ordres potentiellement dangereux.
Tout acquérir ou ne rien acquérir
Une manière stricte de supprimer la possession et attente consiste à accorder en une étape à un fil d’exécution l’ensemble complet de ses ressources, ou à ne rien lui accorder. Il ne détient jamais une partie de l’ensemble en attendant le reste. Cela exige de connaître les ressources nécessaires à l’avance et peut réduire la concurrence.
Un protocole apparenté sérialise les séquences d’acquisition avec un verrou global de prévention :
lock(&prevention);
lock(&L1);
lock(&L2);
unlock(&prevention);
/* use L1 and L2, then release them */
Tous les fils d’exécution doivent finir d’acquérir leur ensemble complet sous ce verrou global, et la libération des ressources ne doit pas nécessiter son acquisition. Un fil peut toujours détenir L1 en attendant L2 : cela ne supprime donc pas littéralement la possession et attente. Le protocole empêche les autres fils de commencer une acquisition partielle concurrente. Aucun détenteur ne peut se trouver au milieu d’une seconde séquence d’acquisition concurrente. Les coûts sont la détention anticipée des verrous et la sérialisation des acquisitions.
Abandonner et réessayer
pthread_mutex_trylock() revient immédiatement avec EBUSY lorsqu’un verrou n’est pas disponible. Un fil d’exécution peut acquérir L1, essayer L2 et libérer L1 si la tentative échoue. Il doit aussi annuler ses autres acquisitions partielles avant de réessayer.
Deux fils d’exécution peuvent entrer en collision à répétition, renoncer puis réessayer ensemble. C’est un verrouillage actif : les deux s’exécutent, mais aucun ne termine. Une temporisation aléatoire réduit les collisions répétées ; elle ne transforme pas la boucle de nouvelles tentatives en garantie d’équité.
Remplacer un verrou par une opération atomique
La comparaison et échange (CAS) ne modifie un emplacement que s’il contient toujours la valeur attendue. En principe, une boucle de nouvelles tentatives pour une addition atomique lit l’ancienne valeur, tente de la remplacer par cette valeur augmentée de l’incrément et réessaie si un autre fil d’exécution l’a modifiée avant. Les implémentations C réelles utilisent des types et des opérations atomiques pour la lecture comme pour la mise à jour.
Supprimer l’acquisition d’un verrou supprime l’interblocage sur ce verrou pour cette opération. Un algorithme sans verrou garantit la progression de l’ensemble du système, mais un fil d’exécution peut échouer à répétition dans ses tentatives. Les structures générales sans verrou exigent aussi une conception attentive de la durée de vie de la mémoire.
| Technique | Principal compromis |
|---|---|
| Acquisition ordonnée | Chaque chemin du code doit respecter l’ordre. |
| Acquisition simultanée | Les ressources doivent être connues et peuvent rester inutilisées. |
| Tentative non bloquante et nouvelle tentative | Le travail de nouvelle tentative et le verrouillage actif exigent de l’attention. |
| Mise à jour sans verrou | Le raisonnement sur la correction est plus exigeant. |
Points clés
- Un ordre commun des verrous ne prévient l’attente circulaire que si tous les chemins le respectent.
- Une véritable acquisition de tout ou rien supprime la possession et attente.
- Un verrou global d’acquisition sérialise les séquences complètes d’acquisition.
- Après l’échec d’une tentative de verrouillage non bloquante, libère les ressources partiellement acquises avant de réessayer.
- Une temporisation aléatoire réduit les collisions liées au verrouillage actif.
- La progression sans verrou du système ne garantit pas celle de chaque fil d’exécution.
4.3 Vérifier la sûreté d’une demande de ressources
L’évitement vérifie si une demande envisagée laisse à chaque fil d’exécution une possibilité de terminer. Il autorise les schémas habituels d’utilisation des ressources, mais retarde les demandes qui détruisent les ordres de terminaison sûrs restants.
Un état sûr garantit qu’un ordre est possible
Une séquence sûre ordonne les fils d’exécution de sorte que chacun puisse obtenir sa demande maximale restante à partir des ressources actuellement libres et de celles que libèrent les fils précédents. Un état non sûr ne possède pas un tel ordre garanti. Il n’est pas nécessairement déjà en interblocage : les fils peuvent demander moins que leur maximum ou libérer des ressources plus tôt.
L’ordonnancement peut aussi éviter un chevauchement dangereux en garantissant que deux tâches incompatibles n’entrent pas ensemble dans leurs phases d’utilisation des ressources. Les placer simplement sur un seul processeur ne suffit pas, car elles peuvent toujours subir une préemption et s’entrelacer tout en détenant des ressources. La garantie nécessaire est l’absence de chevauchement des phases incompatibles.
Préparer les tableaux du banquier
Pour chaque type de ressource, suis Available, le vecteur des ressources disponibles. Pour chaque fil d’exécution, suis Max, Allocation et Need = Max - Allocation. Compare les vecteurs composante par composante, jamais selon la somme de leurs composantes.
Utilise les totaux de ressources (7, 5, 6) :
| Fil d’exécution | Allocation | Max | Besoin |
|---|---|---|---|
| T0 | (1, 1, 0) | (5, 3, 2) | (4, 2, 2) |
| T1 | (2, 0, 1) | (3, 2, 2) | (1, 2, 1) |
| T2 | (1, 1, 2) | (2, 2, 4) | (1, 1, 2) |
| T3 | (0, 1, 1) | (4, 2, 3) | (4, 1, 2) |
Les allocations totalisent (4, 3, 4), ce qui laisse Available = (3, 2, 2).
Effectuer le test de sûreté
Affecte à Work, le vecteur de travail, la valeur Disponible. Choisis un fil d’exécution non terminé dont le besoin ne dépasse pas Travail, simule sa terminaison et ajoute son allocation à Travail.
- Le besoin de T1 est satisfait par (3, 2, 2). Le retour de (2, 0, 1) donne Travail (5, 2, 3).
- Le besoin de T0 est satisfait. Le retour de (1, 1, 0) donne (6, 3, 3).
- Le besoin de T2 est satisfait. Le retour de (1, 1, 2) donne (7, 4, 5).
- Le besoin de T3 est satisfait. Le retour de (0, 1, 1) donne (7, 5, 6).
Chaque fil d’exécution termine dans cette simulation : l’état est donc sûr. Travail augmente de l’allocation, et non du maximum : les ressources supplémentaires accordées temporairement pour terminer sont elles aussi rendues, ce qui donne un gain net égal à ce que le fil détenait initialement.
Simuler l’attribution, puis tester de nouveau
Une demande ne doit dépasser ni le besoin du fil d’exécution ni les ressources disponibles. Soustrais-la ensuite de Disponible, ajoute-la à Allocation, soustrais-la de Besoin et effectue le test de sûreté sur cet état provisoire.
À partir du tableau initial, si T2 demande (1, 0, 1), Disponible devient (2, 2, 1). T1 peut toujours passer en premier, suivi de T0, T2 et T3 : l’état est donc sûr.
Si T3 demande à la place (2, 1, 1), Disponible devient (1, 1, 1). Le besoin restant de T3 est (2, 0, 1). Aucun fil ne peut terminer : T0 a besoin de 4 unités de A, T1 de 2 unités de B, T2 de 2 unités de C et T3 de 2 unités de A. Retarde cette demande malgré les ressources actuellement libres.
Chaque demande ultérieure nécessite le même test. L’évitement exige des demandes maximales déclarées et un modèle de ressources connu, ce qui le rend plus adapté aux systèmes spécialisés prévisibles qu’aux charges de travail des applications généralistes.
Points clés
- Le besoin est égal à la demande maximale moins l’allocation actuelle.
- Une séquence sûre doit satisfaire séparément chaque type de ressource.
- Après avoir simulé la terminaison, ajoute l’allocation au vecteur de travail.
- Une demande ne doit dépasser ni le besoin ni les ressources disponibles et doit laisser un état sûr.
- Un état non sûr ne garantit aucune séquence de terminaison ; il ne signifie pas qu’un interblocage s’est déjà formé.
4.4 Détecter un interblocage et choisir un rétablissement
Un système peut laisser un interblocage se former, le détecter et rétablir son fonctionnement. Il accepte ainsi une perte occasionnelle de travail en échange de moins de restrictions sur les demandes ordinaires de ressources. La détection n’est utile que s’il existe une action de rétablissement possible.
Trouver les cycles de blocage
Pour les ressources à instance unique, maintiens un graphe d’attente et recherche un cycle, par exemple avec un parcours en profondeur. Supposons que les arêtes soient T1 → T2, T2 → T3, T3 → T1 et T4 → T2. T1, T2 et T3 forment le cycle. T4 est bloqué derrière celui-ci ; sa propre exécution ne peut pas libérer une ressource qui brise ce cycle.
Avec plusieurs instances, un cycle ne suffit pas. Utilise les ressources disponibles, l’allocation et la demande actuelle non satisfaite de chaque fil d’exécution. Initialise le vecteur de travail aux ressources disponibles. Trouve à répétition une demande qui peut être satisfaite, simule la terminaison du fil et ajoute son allocation au vecteur de travail. Contrairement à l’évitement, ce test utilise ce que les fils attendent maintenant, et non leur demande maximale future possible.
Le détecteur de ressources standard écarte initialement les fils d’exécution qui ne détiennent aucune ressource : ils ne peuvent pas contribuer par des ressources détenues à un cycle de ressources. Parmi les détenteurs de ressources, ceux que le test laisse non terminés sont en interblocage selon le modèle. D’autres fils peuvent tout de même être incapables de progresser parce qu’ils dépendent de cet ensemble. Distinguer le cycle des fils bloqués qui en dépendent aide à choisir une victime de rétablissement adaptée.
Rétablir le fonctionnement sans corrompre l’état partagé
Un système peut terminer un ou plusieurs processus, récupérer des ressources avec un retour arrière à un point de contrôle ou redémarrer. Choisis les victimes selon le coût du rétablissement, la priorité, le travail déjà effectué et les ressources détenues. Après avoir supprimé une victime, vérifie de nouveau : plusieurs cycles indépendants peuvent nécessiter plusieurs actions de rétablissement.
Terminer un fil d’exécution arbitraire ne suffit pas s’il laisse un mutex verrouillé ou une structure partagée à moitié modifiée. Le rétablissement doit libérer ou reconstruire les ressources et restaurer un état valide. Les systèmes transactionnels peuvent annuler une transaction en interblocage, car le retour arrière fait partie de leur conception. Une application ordinaire nécessite souvent à la place un redémarrage contrôlé du processus.
Interpréter les délais d’attente avec prudence
pthread_mutex_timedlock() peut renvoyer ETIMEDOUT après une échéance. Un chien de garde peut réinitialiser un périphérique qui cesse de signaler sa progression. Les deux détectent une attente excessive ; une attente longue peut aussi provenir d’entrées-sorties lentes ou de l’ordonnancement. L’expiration d’un délai est un signe qui invite à enquêter ou à rétablir le fonctionnement, pas un diagnostic certain de cycle.
| Stratégie | Ce qu’elle nécessite | Coût principal |
|---|---|---|
| Prévention | Une règle de conception qui supprime une condition nécessaire. | Moins de souplesse ou de concurrence. |
| Évitement | Les demandes maximales futures et un test de sûreté. | Demandes retardées et travail de gestion. |
| Détection et rétablissement | Un détecteur et une méthode de rétablissement valide. | Coût des vérifications et perte de travail possible. |
| Ignorer les interblocages rares | Une défaillance et un coût de redémarrage acceptables. | Perturbations occasionnelles. |
Linux et Windows ne recherchent ni ne réparent généralement les cycles entre les verrous propres à une application. Le programme doit mettre en œuvre la stratégie choisie. Si le rétablissement choisit à répétition le même travail comme victime, celui-ci peut subir une famine même si chaque interblocage est résolu.
Points clés
- La détection trouve un interblocage existant ; la prévention empêche sa formation.
- La détection avec plusieurs instances utilise les demandes actuelles, et non le besoin maximal.
- Distingue un cycle de ressources des fils d’exécution bloqués derrière lui.
- Le rétablissement doit restaurer des ressources et un état partagé valides, pas seulement arrêter un fil d’exécution.
- L’expiration d’un délai peut aussi indiquer une opération lente.
5Maintenir la réactivité d’une boucle d’événements
La concurrence peut provenir d’un seul fil d’exécution qui sert de nombreuses tâches pendant que leurs attentes d’entrées-sorties se chevauchent. Une boucle d’événements choisit une tâche prête et exécute un court gestionnaire, puis revient à l’attente de l’événement suivant. Cela supprime le verrouillage entre les gestionnaires synchrones de cette boucle, mais chaque opération bloquante retarde tout le monde. Les vérifications de disponibilité, les entrées-sorties asynchrones et les continuations sauvegardées maintiennent la progression de la boucle ; plusieurs cœurs et l’état partagé exigent toujours de l’attention.
5.1 Traiter les descripteurs disponibles sans se bloquer
Une boucle d’événements attend des événements et répartit de courts traitements entre des gestionnaires sur un seul fil d’exécution. Le programme choisit explicitement le prochain événement disponible à traiter. Node.js et nginx utilisent des conceptions fondées sur les événements pour servir de nombreuses connexions sans attribuer un fil dédié à chacune.
while (1) {
events = get_events();
/* handle each ready event, then return to the loop */
}
Les gestionnaires synchrones d’une même boucle s’exécutent un par un. Les instructions ordinaires d’un gestionnaire ne sont pas interrompues par un autre gestionnaire de cette boucle. La boucle dépend toujours du retour rapide des gestionnaires.
Construire et examiner les ensembles de select
select() accepte des ensembles de descripteurs à surveiller en lecture, en écriture et pour les conditions exceptionnelles, ainsi qu’un délai d’attente. Son argument nfds est égal au plus grand descripteur surveillé plus un. Surveiller les descripteurs 3, 5 et 9 nécessite donc nfds = 10, et non 3.
FD_ZERO(&readset);
FD_SET(3, &readset);
FD_SET(5, &readset);
FD_SET(9, &readset);
int n = select(10, &readset, NULL, NULL, NULL);
/* If n > 0, test each watched descriptor with FD_ISSET. */
Un délai d’attente nul au sens d’un pointeur nul attend qu’un descripteur soit disponible ; un délai de zéro vérifie et revient immédiatement. select() écrase les ensembles pour n’y laisser que les descripteurs disponibles : reconstruis-les donc avant chaque appel. Sa valeur de retour positive compte les bits positionnés dans les ensembles renvoyés ; un descripteur disponible dans deux ensembles est compté deux fois. Zéro indique l’expiration du délai ; −1 indique une erreur.
La disponibilité ne transfère pas les données. Le programme appelle encore read() ou recv(). Une fin d’entrée peut apparaître comme une disponibilité en lecture ; une lecture qui renvoie 0 signifie que l’autre extrémité a fermé son côté émetteur. Traite ce résultat au lieu de réaffecter à répétition le traitement de la même entrée terminée.
Garder les gestionnaires courts et non bloquants
Passe les sockets en mode O_NONBLOCK. Lorsqu’une opération ne peut pas continuer, elle renvoie −1 avec errno défini à EAGAIN ou EWOULDBLOCK, ce qui permet au gestionnaire de revenir à la boucle. La disponibilité peut changer avant l’opération : le mode non bloquant protège donc aussi contre une attente inattendue après un signalement de disponibilité.
Une lecture bloquante dans un gestionnaire retarde chaque événement en file d’attente. Avec 100 requêtes en attente et une lecture disque bloquante de 10 ms par gestionnaire, la dernière requête attend environ 990 ms avant que sa propre lecture ne commence ; le lot prend environ 1 000 ms. Le fil d’exécution a passé la majeure partie de ce temps à attendre alors que du travail prêt restait en file.
Choisir une interface de disponibilité
| Interface | Représentation de l’ensemble surveillé | Propriété importante |
|---|---|---|
select() | Ensembles de bits de descripteurs reconstruits à chaque appel. | Les valeurs des descripteurs doivent être inférieures à FD_SETSIZE, généralement 1024. |
poll() | Un tableau de descripteurs et d’événements demandés ou renvoyés. | Aucune limite fixe correspondante sur la valeur des descripteurs. |
epoll / kqueue | Un ensemble d’événements à surveiller enregistré et maintenu par le noyau. | Transmission efficace des événements pour de grands ensembles surveillés. |
select() et poll() examinent l’ensemble surveillé fourni à chaque appel : leur travail augmente donc avec sa taille. Linux propose epoll ; FreeBSD et macOS proposent kqueue. Ne surveiller que quelques descripteurs ne rend pas utilisable une valeur élevée de descripteur avec un fd_set de taille fixe.
Points clés
- Une boucle répartit les traitements des gestionnaires synchrones un par un.
- Pour select, nfds est égal au plus grand descripteur surveillé plus un.
- Reconstruis les ensembles de select après chaque appel.
- La disponibilité nécessite ensuite un appel d’entrée-sortie ; une lecture qui renvoie 0 indique une fin d’entrée.
- Les sockets non bloquantes permettent aux gestionnaires de revenir à la boucle lorsqu’une opération devrait attendre.
- FD_SETSIZE limite les valeurs des descripteurs, et pas seulement leur nombre.
5.2 Sauvegarder le travail qui continue après une entrée-sortie
La disponibilité réseau indique à une boucle quand elle peut tenter une entrée-sortie. Les fichiers disque ordinaires sont différents : select() et poll() les indiquent comme disponibles même lorsque l’accès à leurs données attend encore le stockage. Pour continuer à traiter d’autres événements, soumets le travail de façon asynchrone et traite son résultat plus tard.
Soumettre, travailler ailleurs, récupérer
Les entrées-sorties asynchrones POSIX enregistrent une requête dans une struct aiocb. Les champs importants sont aio_fildes pour le descripteur de fichier, aio_offset pour le déplacement dans le fichier, aio_buf pour la destination et aio_nbytes pour la longueur demandée.
/* cb describes a read request */
aio_read(&cb);
/* Return to other work. Later: */
int status = aio_error(&cb);
/* EINPROGRESS: still running; 0: succeeded; otherwise an error. */
/* After completion, aio_return(&cb) collects its result. */
aio_read() place la requête en file d’attente et revient. La fin de l’opération est un événement distinct. Lorsque aio_error() indique la fin, aio_return() fournit le résultat, par exemple le nombre d’octets lus en cas de succès. Une requête peut échouer et une lecture réussie peut renvoyer moins d’octets que demandé. L’enregistrement de la requête et son tampon doivent rester valides tant que l’opération est en cours.
Parcourir sans cesse toutes les requêtes dans une boucle rapide gaspille du temps processeur. aio_sigevent peut configurer un signal ou un rappel de fin. io_uring de Linux utilise des files de soumission et de complétion ; les ports de complétion d’entrées-sorties de Windows répartissent les traitements selon les opérations terminées. Ces interfaces permettent au programme d’agir sur le travail terminé sans vérifier constamment chaque requête inachevée.
Conserver l’état pendant l’intervalle
Un fil d’exécution bloqué conserve les variables locales de sa fonction sur sa pile. Un gestionnaire d’événement qui soumet une entrée-sortie et revient doit sauvegarder ce dont le gestionnaire ultérieur aura besoin. Cette gestion explicite est la gestion manuelle de la pile ; l’état sauvegardé est une continuation.
Supposons qu’un serveur lise le descripteur de fichier 12 pour un client sur la socket 7. Lors de la soumission, il sauvegarde un enregistrement qui associe cette requête de lecture à la socket 7 et à l’état de sa réponse. Lorsque la fin de l’opération est signalée, la boucle trouve l’enregistrement, vérifie le résultat de la lecture et continue à envoyer la réponse au bon client. Des enregistrements distincts empêchent de confondre les destinations de plusieurs requêtes en cours.
La continuation doit être liée à l’opération précise, et non à un simple identifiant qui pourrait être réutilisé plus tard. Garde la référence au client valide jusqu’au traitement de la fin de l’opération et supprime l’enregistrement lorsque la tâche se termine. Ces détails de durée de vie remplacent les hypothèses qu’une fonction bloquante à exécution linéaire pourrait conserver sur sa pile.
Une syntaxe lisible peut masquer une suspension
Les rappels, les promesses et async/await automatisent une grande partie de cette sauvegarde de l’état. En JavaScript, const data = await readFile(path); suspend cette fonction pendant que son entrée-sortie est en attente. D’autres travaux de la boucle peuvent s’exécuter et la fonction reprend plus tard avec son état local restauré.
Asynchrone signifie que le fil appelant peut continuer avant la fin de l’opération. Cela ne signifie pas que ses gestionnaires s’exécutent simultanément sur plusieurs cœurs de processeur. Cela ne fait pas non plus céder automatiquement le processeur au code exigeant en calcul après un await : ce code occupe toujours son fil d’exécution jusqu’à ce qu’il revienne ou se suspende à nouveau.
Points clés
- La disponibilité d’un fichier ordinaire ne garantit pas qu’une lecture disque évitera une attente.
- Les entrées-sorties asynchrones séparent la soumission de la fin de l’opération et de la récupération du résultat.
- Une continuation sauvegarde l’état nécessaire après le retour du gestionnaire initial.
- Garde les enregistrements des requêtes et les tampons valides jusqu’à la fin des opérations.
- Async/await sauvegarde l’état pendant une suspension ; il ne crée pas d’exécution multicœur.
5.3 Choisir les événements, les fils d’exécution ou une solution hybride
Une boucle d’événements convient bien à de nombreuses connexions pour la plupart inactives, ponctuées de courtes périodes de travail. Une conception fondée sur les fils d’exécution conserve ensemble le déroulement d’une tâche et sa pile. Le meilleur choix dépend du temps d’attente, du travail du processeur, de la mémoire et de l’évolution prévue du code.
Comparer le travail lié à chaque conception
| Aspect | Conception fondée sur les événements | Conception fondée sur les fils d’exécution |
|---|---|---|
| Connexions surtout inactives | Petit enregistrement d’état par connexion. | Un fil entraîne un surcoût de pile et d’ordonnancement. |
| Travail bloquant | Le confier à un travailleur ou le scinder autour de sa fin. | Le fil appelant peut attendre pendant que les autres s’exécutent. |
| Plusieurs cœurs de processeur | Exécuter plusieurs boucles ou utiliser des travailleurs. | Répartir les fils prêts sur les cœurs. |
| Données partagées modifiables | Une boucle synchrone évite le chevauchement des gestionnaires. | Les accès partagés nécessitent une synchronisation. |
| Une fonction devient bloquante | Ses appelants peuvent devoir être restructurés autour d’une continuation. | Le déroulement linéaire peut être conservé. |
Un serveur de discussion avec 50 000 connexions pour la plupart inactives peut tirer profit d’enregistrements d’événements au lieu d’une pile de fil d’exécution dédiée par connexion. Un programme de traitement d’images qui effectue des calculs importants par requête a besoin d’assez de travailleurs pour utiliser les cœurs disponibles ; une seule boucle occupée ne peut pas paralléliser ces calculs.
Les attentes cachées comptent aussi
Un gestionnaire peut se bloquer sans appel évident à une API bloquante. L’accès à une page transférée sur disque peut déclencher une lecture disque. Pendant ce défaut de page, le fil d’exécution de la boucle attend. mlock() peut maintenir les pages nécessaires en mémoire lorsque le système autorise la quantité de mémoire verrouillée demandée. Supprimer les appels explicitement bloquants n’élimine pas toutes les sources d’attente.
Les modifications du code peuvent introduire une autre attente cachée. Si une fonction auxiliaire auparavant immédiate commence à récupérer des données, un appelant fondé sur les événements doit scinder son travail autour d’une continuation ou confier l’appel à un travailleur. La pile d’un fil bloquant conserve déjà le point de reprise et l’état local nécessaire.
Combiner les points forts
Une conception hybride garde la gestion de la disponibilité réseau dans la boucle d’événements et envoie les tâches bloquantes ou exigeantes en calcul à un pool de travailleurs. Les fins de ces tâches sont transmises à la boucle pour le traitement des réponses. Node.js utilise le pool de fils d’exécution de libuv pour les opérations sur le système de fichiers : une interface asynchrone peut donc être mise en œuvre par des fils en arrière-plan.
Pour utiliser plusieurs cœurs, un serveur peut exécuter une boucle d’événements par cœur dans des fils d’exécution ou des processus distincts. Si ces boucles partagent des données modifiables, elles ont besoin des verrous habituels ou d’un autre protocole de synchronisation. L’absence de verrous vaut pour les gestionnaires synchrones d’une même boucle, pas pour toutes les parties d’un serveur fondé sur les événements.
Une tâche peut s’entrelacer autour de await
Supposons qu’il reste une unité en stock. Un gestionnaire vérifie sa disponibilité, attend le paiement, puis la déduit du stock. Pendant sa suspension, un autre gestionnaire peut vérifier le même stock et commencer lui aussi un paiement. Les deux vérifications peuvent réussir avant l’une ou l’autre des déductions. Aucun couple de gestionnaires synchrones ne s’exécute simultanément, mais l’opération globale de vérification et de mise à jour n’est pas atomique.
Réserve le stock par une modification synchronisée unique de l’état avant de céder l’exécution, avec une procédure de libération si le paiement échoue, ou vérifie de nouveau selon un protocole qui empêche une seconde vente. Un mutex détenu pendant une longue attente peut créer d’autres problèmes de progression : conçois donc explicitement la transition d’état et son rétablissement. Le code événementiel exige toujours de déterminer quels faits restent vrais pendant chaque suspension.
Points clés
- Les événements conviennent à de nombreuses connexions inactives ; les travaux exigeants en calcul nécessitent des travailleurs parallèles.
- Une boucle d’événements utilise un cœur ; les données partagées entre boucles nécessitent une synchronisation.
- Les défauts de page et les fonctions auxiliaires devenues bloquantes peuvent bloquer une boucle.
- Les serveurs hybrides confient le travail bloquant à des travailleurs et transmettent les fins d’opérations à la boucle.
- Une tâche qui s’étend de part et d’autre de await peut subir une violation d’atomicité entre vérification et action.
Sémaphores et interblocage : exemples
1D’un indicateur de fin à une file fonctionnelle
12 étapes
- 1IntroductionAttendre sans gaspiller le processeur
Un fil père a besoin du résultat de son fil fils, mais celui-ci peut terminer avant ou après que le père commence à attendre. Un protocole fonctionnel doit gérer les deux ordres.
Utilise un indicateur partagé
donepour mémoriser la fin du travail, un mutex pour le protéger et une variable de condition pour permettre au père de se mettre en sommeil. Suis le passage du mutex pendant l’exécution de chaque fil. - 2ExempleLe père arrive en premier
Au départ,
done = 0. Le père verrouillem, vérifie l’indicateur et appellewait(&c, &m)dans une bouclewhile.L’attente libère le mutex et commence l’attente de façon atomique. Le fils peut maintenant acquérir
m. Il n’existe aucun intervalle sans verrou entre la vérification de la condition et l’entrée en attente pendant lequel le fils pourrait envoyer une notification que le père manquerait. - 3ExempleLe fils enregistre la fin du travail
Le fils acquiert
m, enregistredone = 1, signalecet libère le verrou. Le signalement rend le père prêt à s’exécuter ; il ne lui donne pas immédiatement l’exécution.Le père reprend
mavant le retour de l’attente, vérifie de nouveaudoneet continue, car la fin du travail du fils est désormais enregistrée. Les deux fils d’exécution utilisent le même mutex pour l’indicateur partagé. - 4DéfiInverse leur ordre d’arrivée
Le fils termine et signale avant que le père n’ait atteint son attente. Qu’est-ce qui empêche le père de rester en sommeil indéfiniment ?
---reveal---
Le fils a déjà affecté
done = 1sous la protection du mutex. Le père vérifie cet indicateur en détenant le mutex et n’attend pas. Le signalement précoce n’est pas conservé ; l’indicateur d’état transmet l’information malgré l’ordre d’arrivée différent. - 5ExempleUn réveil ne réserve pas un élément
C1 attend un tampon d’une place. Un producteur insère un élément et signale C1. Avant que C1 ne reprenne le mutex, C2 l’acquiert et retire cet élément.
Selon la sémantique de Mesa, C1 est seulement prêt à entrer en concurrence. Il doit tester de nouveau le tampon après avoir acquis le mutex. Le retour de l’attente ne remplace pas la vérification de la présence de l’élément.
- 6DéfiChoisis la bonne vérification
Dans cette trace, C1 utilisait
if (count == 0) wait(...). Il revient maintenant après que C2 a vidé la place. Quelle modification du code empêche un retrait invalide ?---reveal---
Utilise
while (count == 0) wait(...). Après avoir repris le mutex, C1 voit que le tampon est vide et attend à nouveau. La même boucle gère aussi les réveils intempestifs : elle est donc nécessaire même sans second consommateur. - 7ExempleRéveille chaque fil dont la demande peut être satisfaite
Un allocateur dispose de 0 octet libre. Ta attend 100 octets et Tb en attend 10. La libération de 50 octets ne permet qu’à Tb de continuer, mais un signalement pourrait réveiller Ta.
Une diffusion atteint les deux. Chacun reprend le mutex et vérifie la quantité qu’il a demandée. Tb peut prendre 10 octets ; Ta attend à nouveau. La boucle de vérification du prédicat assure la correction tandis que la diffusion évite de laisser endormi un fil qui peut continuer.
- 8ExempleConserve l’ordre de la file lors du retour au début
La capacité est de 3. Insère 7, 8, 9 ; retire 7 ; insère 10. Le tableau physique est maintenant
[10, 8, 9].L’indice de lecture pointe vers la place 1 : l’ordre de retrait est donc 8, 9, 10. Avance les deux indices modulo la capacité et modifie
countsous la protection du mutex. L’affichage du tableau de gauche à droite ne correspond pas nécessairement à l’ordre logique de la file. - 9Point cléDonne à chaque rôle sa propre file
Un producteur attend sur
emptytant que le tampon est plein, insère, puis signalefill. Un consommateur attend surfilltant que le tampon est vide, retire, puis signaleempty. Les deux variables de condition utilisent le mutex du tampon.Les files distinctes sont importantes : une notification d’élément atteint un consommateur et une notification de place atteint un producteur. Une seule file de signalement partagée pourrait choisir un fil d’exécution dont la condition est encore fausse.
- 10DéfiTrouve le producteur endormi
Un tampon d’une place utilise une seule file de condition partagée et des boucles
while. P est endormi parce que le tampon est plein. C1 retire l’élément, signale C2, puis attend parce que le tampon est vide. C2 trouve aussi le tampon vide et attend. Pourquoi la place reste-t-elle inutilisée ?---reveal---
P n’a jamais reçu la notification de place libre. Tout le travail susceptible de s’exécuter s’est arrêté alors qu’un producteur pourrait continuer. Des files distinctes pour les producteurs et les consommateurs dirigent le signalement de C1 vers P.
- 11DéfiTermine le travail sans perdre d’éléments
Deux producteurs génèrent encore des requêtes. Un coordinateur prévoit d’ajouter maintenant un marqueur d’arrêt pour chaque consommateur. Que doit-il changer, et pourquoi ?
---reveal---
Attends d’abord que tous les producteurs aient terminé. Place les marqueurs d’arrêt dans la file seulement ensuite. Sinon, les consommateurs peuvent prendre les marqueurs et quitter leur boucle alors que les producteurs ont encore des requêtes réelles à insérer. La synchronisation normale de la file reste utilisée pour insérer et retirer les marqueurs.
- 12RécapitulatifExamine une attente sur condition
Pour chaque attente, identifie l’état, le mutex et la file. Vérifie le prédicat sous la protection du mutex, entre en attente sans intervalle non verrouillé et vérifie de nouveau après chaque retour.
Examine ensuite le chemin de signalement : a-t-il modifié l’état et la notification peut-elle atteindre un fil d’exécution qui peut maintenant continuer ? Ces questions révèlent les réveils perdus comme les blocages du tampon dus au réveil du mauvais rôle.
2Suis les jetons dans des protocoles réels
15 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionPrécise ce que représente un jeton
- 2ExempleSuis un sémaphore binaire
- 3DéfiCorrige un compte initial erroné
- 4ExempleMémorise une fin précoce
- 5ExempleLimite une phase gourmande en mémoire
- 6DéfiDistingue l’admission de la gestion des ressources
- 7LienRetrouve l’autre fil avant de continuer
- 8ExplicationRéserve, mets à jour, rends disponible
- 9ExempleSuis un consommateur bloqué
- 10DéfiRepère la dépendance au verrou
- 11ExplicationLaisse un groupe de lecteurs détenir un jeton
- 12ExempleObserve le rédacteur en attente
- 13DéfiGère une mise à jour nécessaire
- 14DéfiConserve les deux types de protection
- 15RécapitulatifExamine chaque jeton acquis
3Diagnostique l’entrelacement caché
9 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionDemande-toi ce que suppose le code
- 2ExemplePlace une modification dans l’intervalle
- 3ExempleRends la décision numérique indivisible
- 4DéfiTrouve la délimitation manquante de la section critique
- 5ExempleUn nouveau travailleur peut s’exécuter en premier
- 6ExplicationSignale que l’initialisation est terminée
- 7DéfiRemplace une estimation du délai
- 8DéfiChoisis entre les deux corrections
- 9RécapitulatifTeste la garantie, pas le délai
4D’un cycle de verrous à une demande sûre
17 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionDistingue trois stratégies
- 2ExempleFerme un cycle entre deux fils
- 3ExplicationNomme les conditions nécessaires
- 4DéfiDistingue une chaîne d’un cycle
- 5ExempleOrdonne les deux sens de la même façon
- 6ConseilLibère les ressources partiellement acquises avant de réessayer
- 7DéfiExamine la fonction auxiliaire cachée
- 8ExempleCalcule les demandes restantes
- 9ExempleTrouve un fil qui peut terminer en premier
- 10ExempleTermine la séquence sûre
- 11Point cléUne attribution de ressources libres peut rester non sûre
- 12DéfiCompare les ressources par type
- 13LienDétecte l’attente actuelle
- 14ExplicationRétablis un état valide
- 15DéfiChoisis une cible de rétablissement utile
- 16DéfiInterprète un résultat non sûr
- 17RécapitulatifAdapte la vérification à la question
5Traite les événements pendant les entrées-sorties en attente
13 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionDéplace l’attente à la frontière de la boucle
- 2ExempleConstruis l’ensemble surveillé par select
- 3ExplicationEffectue la lecture
- 4DéfiVérifie un descripteur de valeur élevée
- 5ExplicationNe confonds pas disponibilité d’un fichier et fin de lecture
- 6ExempleSauvegarde la destination de la réponse
- 7DéfiInterprète une opération en cours
- 8LienLaisse await préserver l’état local
- 9ExempleUtilise des travailleurs pour le travail coûteux
- 10ConseilRepère une attente implicite
- 11DéfiTrouve l’intervalle autour de await
- 12DéfiGarde visible la frontière entre les cœurs
- 13RécapitulatifExamine chaque point de suspension
Sémaphores et interblocage : QCM
Série 1 : Suis le protocole dans un ordonnancement10 Questions Pratiques
Question 1 sur 10
Afficher la réponseMasquer la réponse
Question 2 sur 10
Afficher la réponseMasquer la réponse
Question 3 sur 10
Afficher la réponseMasquer la réponse
Question 4 sur 10
Afficher la réponseMasquer la réponse
writelock. Le groupe détient un seul jeton de ressource, et non un jeton distinct que le rédacteur doit recueillir auprès de chaque lecteur.Question 5 sur 10
Afficher la réponseMasquer la réponse
Question 6 sur 10
Afficher la réponseMasquer la réponse
Question 7 sur 10
Afficher la réponseMasquer la réponse
Question 8 sur 10
Afficher la réponseMasquer la réponse
Question 9 sur 10
Afficher la réponseMasquer la réponse
Question 10 sur 10
Afficher la réponseMasquer la réponse
Série 2 : Attentes, jetons et dépendances10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 3 : Applique les règles lorsque les conditions changent10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 4 : Analyse des corrections incomplètes10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 5 : Applique le raisonnement à de nouvelles conceptions10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Tu bloques sur ton propre exercice ? Aide aux Devoirs
Mots-Clés
A
Absence de préemption
Admission équitable
Algorithme du banquier
Attente circulaire
B
Bloc de contrôle d’entrée-sortie asynchrone
Blocage implicite
Boucle d’événements
C
Chien de garde
Comparaison et échange
Concurrence hybride
Condition couvrante
Continuation
Contrôle d’admission
Conversion d’un verrou de lecture en verrou d’écriture
D
Détection des interblocages
Diffusion
Disponibilité pour une entrée-sortie
E
Ensemble de descripteurs
select() le remplace par le sous-ensemble disponible.Entrées-sorties asynchrones
Entrées-sorties non bloquantes
EAGAIN et le gestionnaire revient à la boucle d’événements.Entrelacement
epoll
Exclusion mutuelle
Expiration du délai d’attente
É
État non sûr
État sûr
Évitement des interblocages
F
Famine
Famine des rédacteurs
File de complétion
G
Gestion manuelle de la pile
Graphe d’attente
I
Indicateur de disponibilité
ready qu’après avoir affecté l’enregistrement que le travailleur utilisera.Interblocage
io_uring
J
Jeton
Jonction
K
kqueue
L
Lecture et addition
Libération et mise en attente atomiques
Limitation de la concurrence
M
Marqueur de fin de données
Mise à jour perdue
Modèle à compteur signé
Moniteur
wait().Mutex du tampon
fill_ptr, sous la protection du même mutex.O
Opération P sur un sémaphore
Opération V sur un sémaphore
Ordre d’acquisition des verrous
P
Parallélisme multicœur
poll
Pool de travailleurs
Possession du sémaphore
Possession et attente
Prédicat
done != 0 permet à un fil père de vérifier si son fil fils a terminé.Prévention des interblocages
Priorité aux lecteurs
Priorité aux rédacteurs
Problème des producteurs et des consommateurs
R
Rendez-vous
Réservation de ressource
Retour arrière
Réveil intempestif
Réveil perdu
S
select
Sémantique de Hoare
Sémantique de Mesa
Sémaphore
Sémaphore à compteur
Sémaphore binaire
Sémaphore des éléments disponibles
Sémaphore des places libres
Séquence sûre
Signalement
Suspension
T
Tampon borné
Tampon circulaire
Temporisation aléatoire
Tentative de verrouillage non bloquante
V
Variable d’état
Variable de condition
Vecteur de travail
Vecteur des besoins restants
Vecteur des ressources disponibles
Vérification puis action
Verrou lecteurs-rédacteurs
Verrouillage actif
Victime du rétablissement
Violation d’atomicité
Violation d’ordre
À propos de cette unité
Crée ton propre Matériel d'Étude
Transforme tes notes, diapos et documents en Guides d'Étude, Questions Pratiques, Flashcards et 6 autres outils.