Variables de condition, sémaphores et interblocage

Les verrous empêchent les fils d’exécution de se gêner, mais ceux-ci doivent aussi s’attendre les uns les autres. Ce cours montre comment les variables de condition et les sémaphores mettent un fil d’exécution en sommeil jusqu’à ce que des données soient prêtes, qu’une place soit libre ou qu’un autre fil ait terminé, et les utilise pour résoudre les problèmes des producteurs et des consommateurs et des lecteurs et des rédacteurs. Il aborde ensuite les problèmes qui surviennent dans le code concurrent réel : les violations d’atomicité et d’ordre, ainsi que l’interblocage, et les moyens de prévenir, d’éviter et de détecter ce dernier. Il se termine par la concurrence fondée sur les événements, dans laquelle un seul fil d’exécution exécute une boucle d’événements au lieu d’utiliser de nombreux fils. Les exemples sont écrits en C avec les fils d’exécution POSIX et suivis étape par étape.

IntermédiaireUnité 6 sur 9 de la formation Formation : Systèmes d’exploitation : processus, mémoire et fichiers

Ce que tu vas apprendre

  1. 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 active
      Explique 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 diffusion
      Applique 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 condition
      Construis 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.
  2. 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 verrou
      Dé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émaphores
      Utilise 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émaphores
      Ré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édacteurs
      Autorise 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.
  3. 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’ordre
      Reconnais 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.
  4. 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 produit
      Dé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écessaires
      Conç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 banquier
      Utilise 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 fonctionnement
      Laisse 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.
  5. 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’état
      Lance 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écution
      Compare 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.

Sémaphores et interblocage : cours

5 chapitres

1

Attendre 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émantiqueCe qu’un signalement apporte au fil en attente
MesaL’état prêt ; le fil en attente doit reprendre le mutex et vérifier de nouveau l’état.
HoareLa 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.
2

Compter 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.

UsageValeur initialeSignification
Verrou1Un fil d’exécution peut entrer immédiatement.
Ordre des événements0L’événement ne s’est pas produit.
Ensemble de ressourcesNN 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 :

  1. T0 effectue une opération P : 1 devient 0, donc T0 entre.
  2. T1 effectue une opération P : 0 devient −1, donc T1 se bloque.
  3. T0 effectue une opération V : −1 devient 0, ce qui permet à T1 de continuer.
  4. 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.

PolitiqueChoix d’admissionRisque
Priorité aux lecteursDe nouveaux lecteurs peuvent rejoindre les lecteurs actifs.Les rédacteurs peuvent subir une famine.
Priorité aux rédacteursLes rédacteurs en attente empêchent l’entrée de nouveaux lecteurs.Les lecteurs peuvent subir une famine.
Admission équitableLes 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.
3

Trouver 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éeType de bogueGarantie 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’ordreUne 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é.
4

Raisonner 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écessaireSignification dans l’exemple des verrous
Exclusion mutuelleUn seul fil d’exécution détient un verrou donné.
Possession et attenteChacun détient un verrou en attendant un autre.
Absence de préemptionUn verrou détenu n’est pas retiré de force.
Attente circulaireChacun 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.

TechniquePrincipal compromis
Acquisition ordonnéeChaque chemin du code doit respecter l’ordre.
Acquisition simultanéeLes ressources doivent être connues et peuvent rester inutilisées.
Tentative non bloquante et nouvelle tentativeLe travail de nouvelle tentative et le verrouillage actif exigent de l’attention.
Mise à jour sans verrouLe 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écutionAllocationMaxBesoin
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.

  1. Le besoin de T1 est satisfait par (3, 2, 2). Le retour de (2, 0, 1) donne Travail (5, 2, 3).
  2. Le besoin de T0 est satisfait. Le retour de (1, 1, 0) donne (6, 3, 3).
  3. Le besoin de T2 est satisfait. Le retour de (1, 1, 2) donne (7, 4, 5).
  4. 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égieCe qu’elle nécessiteCoût principal
PréventionUne règle de conception qui supprime une condition nécessaire.Moins de souplesse ou de concurrence.
ÉvitementLes demandes maximales futures et un test de sûreté.Demandes retardées et travail de gestion.
Détection et rétablissementUn détecteur et une méthode de rétablissement valide.Coût des vérifications et perte de travail possible.
Ignorer les interblocages raresUne 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.
5

Maintenir 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é
InterfaceRepré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 / kqueueUn 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
AspectConception fondée sur les événementsConception fondée sur les fils d’exécution
Connexions surtout inactivesPetit enregistrement d’état par connexion.Un fil entraîne un surcoût de pile et d’ordonnancement.
Travail bloquantLe 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 processeurExécuter plusieurs boucles ou utiliser des travailleurs.Répartir les fils prêts sur les cœurs.
Données partagées modifiablesUne boucle synchrone évite le chevauchement des gestionnaires.Les accès partagés nécessitent une synchronisation.
Une fonction devient bloquanteSes 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

5 Leçons Interactives

1

D’un indicateur de fin à une file fonctionnelle

12 étapes
Suis la mise en sommeil, le réveil, la nouvelle vérification et l’acheminement des notifications entre producteurs et consommateurs.
  1. 1
    IntroductionAttendre 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é done pour 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.

  2. 2
    ExempleLe père arrive en premier

    Au départ, done = 0. Le père verrouille m, vérifie l’indicateur et appelle wait(&c, &m) dans une boucle while.

    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.

  3. 3
    ExempleLe fils enregistre la fin du travail

    Le fils acquiert m, enregistre done = 1, signale c et 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 m avant le retour de l’attente, vérifie de nouveau done et 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é.

  4. 4
    Dé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 = 1 sous 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.

  5. 5
    ExempleUn 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.

  6. 6
    Dé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.

  7. 7
    ExempleRé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.

  8. 8
    ExempleConserve 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 count sous la protection du mutex. L’affichage du tableau de gauche à droite ne correspond pas nécessairement à l’ordre logique de la file.

  9. 9
    Point cléDonne à chaque rôle sa propre file

    Un producteur attend sur empty tant que le tampon est plein, insère, puis signale fill. Un consommateur attend sur fill tant que le tampon est vide, retire, puis signale empty. 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.

  10. 10
    Dé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.

  11. 11
    Dé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.

  12. 12
    Ré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.

2

Suis les jetons dans des protocoles réels

15 étapes
Choisis les comptes, suis les passages de relais et distingue les réservations de ressources de l’exclusion mutuelle.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionPrécise ce que représente un jeton
  2. 2
    ExempleSuis un sémaphore binaire
  3. 3
    DéfiCorrige un compte initial erroné
  4. 4
    ExempleMémorise une fin précoce
  5. 5
    ExempleLimite une phase gourmande en mémoire
  6. 6
    DéfiDistingue l’admission de la gestion des ressources
  7. 7
    LienRetrouve l’autre fil avant de continuer
  8. 8
    ExplicationRéserve, mets à jour, rends disponible
  9. 9
    ExempleSuis un consommateur bloqué
  10. 10
    DéfiRepère la dépendance au verrou
  11. 11
    ExplicationLaisse un groupe de lecteurs détenir un jeton
  12. 12
    ExempleObserve le rédacteur en attente
  13. 13
    DéfiGère une mise à jour nécessaire
  14. 14
    DéfiConserve les deux types de protection
  15. 15
    RécapitulatifExamine chaque jeton acquis
3

Diagnostique l’entrelacement caché

9 étapes
Distingue une opération qui doit rester indivisible d’une opération qui doit avoir lieu en premier.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionDemande-toi ce que suppose le code
  2. 2
    ExemplePlace une modification dans l’intervalle
  3. 3
    ExempleRends la décision numérique indivisible
  4. 4
    DéfiTrouve la délimitation manquante de la section critique
  5. 5
    ExempleUn nouveau travailleur peut s’exécuter en premier
  6. 6
    ExplicationSignale que l’initialisation est terminée
  7. 7
    DéfiRemplace une estimation du délai
  8. 8
    DéfiChoisis entre les deux corrections
  9. 9
    RécapitulatifTeste la garantie, pas le délai
4

D’un cycle de verrous à une demande sûre

17 étapes
Suis un interblocage, supprime une condition, vérifie la sûreté des ressources et prévois le rétablissement.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionDistingue trois stratégies
  2. 2
    ExempleFerme un cycle entre deux fils
  3. 3
    ExplicationNomme les conditions nécessaires
  4. 4
    DéfiDistingue une chaîne d’un cycle
  5. 5
    ExempleOrdonne les deux sens de la même façon
  6. 6
    ConseilLibère les ressources partiellement acquises avant de réessayer
  7. 7
    DéfiExamine la fonction auxiliaire cachée
  8. 8
    ExempleCalcule les demandes restantes
  9. 9
    ExempleTrouve un fil qui peut terminer en premier
  10. 10
    ExempleTermine la séquence sûre
  11. 11
    Point cléUne attribution de ressources libres peut rester non sûre
  12. 12
    DéfiCompare les ressources par type
  13. 13
    LienDétecte l’attente actuelle
  14. 14
    ExplicationRétablis un état valide
  15. 15
    DéfiChoisis une cible de rétablissement utile
  16. 16
    DéfiInterprète un résultat non sûr
  17. 17
    RécapitulatifAdapte la vérification à la question
5

Traite les événements pendant les entrées-sorties en attente

13 étapes
Construis une boucle de disponibilité, sauvegarde les continuations et localise les risques de concurrence restants.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionDéplace l’attente à la frontière de la boucle
  2. 2
    ExempleConstruis l’ensemble surveillé par select
  3. 3
    ExplicationEffectue la lecture
  4. 4
    DéfiVérifie un descripteur de valeur élevée
  5. 5
    ExplicationNe confonds pas disponibilité d’un fichier et fin de lecture
  6. 6
    ExempleSauvegarde la destination de la réponse
  7. 7
    DéfiInterprète une opération en cours
  8. 8
    LienLaisse await préserver l’état local
  9. 9
    ExempleUtilise des travailleurs pour le travail coûteux
  10. 10
    ConseilRepère une attente implicite
  11. 11
    DéfiTrouve l’intervalle autour de await
  12. 12
    DéfiGarde visible la frontière entre les cœurs
  13. 13
    RécapitulatifExamine chaque point de suspension

Sémaphores et interblocage : QCM

50 Questions Pratiques en 5 séries

Série 1 : Suis le protocole dans un ordonnancement10 Questions Pratiques

Question 1 sur 10

Un tampon circulaire a une capacité de 4 et commence vide avec les deux indices à 0. Il reçoit 21, 22, 23, retire deux éléments, puis reçoit 24 et 25. Quel est maintenant son ordre logique de retrait ?
  1. 25, 23, 24
  2. 23, 24, 25
  3. 24, 25, 23
  4. 21, 24, 25
Afficher la réponseMasquer la réponse
Réponse : B
Les deux premiers retraits consomment 21 et 22, ce qui laisse 23 comme élément le plus ancien. Insérer 24 puis 25 ramène l’indice d’écriture au début, mais ne fait pas dépasser 23 à l’indice de lecture. Lire le tableau physique à partir de la place 0 placerait à tort la valeur 25, revenue au début, avant les éléments plus anciens.

Question 2 sur 10

Un chargeur positionne ready sous la protection d’un mutex et signale avant qu’un utilisateur ne commence à attendre. L’utilisateur verrouille ensuite ce mutex et teste ready avant de décider s’il doit attendre. Pourquoi peut-il continuer ?
  1. La variable de condition a mémorisé le signalement.
  2. Le verrouillage du mutex rejoue la notification.
  3. Le signalement a transmis le mutex à l’utilisateur ultérieur.
  4. L’état prêt persiste après le signalement.
Afficher la réponseMasquer la réponse
Réponse : D
L’indicateur ready enregistre la fin du chargement, que quelqu’un ait attendu ou non au moment du signalement. L’utilisateur voit cet état sous la protection du mutex et n’attend pas. La variable de condition n’a pas sauvegardé de notification et l’acquisition du mutex n’en rejoue pas une.

Question 3 sur 10

Un tampon utilisant des sémaphores a une capacité de 6 et contient initialement deux éléments. Aucune opération n’est en cours. Quels sont ses comptes de jetons empty et full ?
  1. Empty 4 ; full 2.
  2. Empty 6 ; full 2.
  3. Empty 2 ; full 4.
  4. Empty 4 ; full 0.
Afficher la réponseMasquer la réponse
Réponse : A
Deux éléments rendus disponibles donnent deux jetons full, et les quatre places inoccupées donnent quatre jetons empty. Ces comptes décrivent le tampon une fois les opérations terminées, car aucune opération n’a réservé de jeton sans achever sa mise à jour. Empty n’est pas toujours égal à la capacité ; chaque place occupée retire un jeton de place disponible.

Question 4 sur 10

Trois lecteurs détiennent le verrou de base donnant la priorité aux lecteurs et un rédacteur attend. Deux lecteurs partent. Qu’est-ce qui empêche encore le rédacteur d’entrer ?
  1. Le rédacteur doit recueillir trois jetons d’écriture distincts.
  2. Le dernier lecteur parti a déjà libéré writelock.
  3. Le lecteur restant détient encore l’accès du groupe.
  4. Le verrou du compteur reste détenu par le rédacteur en attente.
Afficher la réponseMasquer la réponse
Réponse : C
Le compteur de lecteurs est passé de trois à un : la transition du dernier lecteur n’a donc pas eu lieu. Seul le lecteur qui ramène le compteur à zéro effectue une opération V sur 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

Un solde partagé utilise des lectures et des soustractions atomiques. Deux fils d’exécution vérifient chacun la présence d’au moins 45 unités, puis soustraient 45 ; le solde initial est de 70. Quel défaut subsiste si les deux vérifications se terminent d’abord ?
  1. La seconde soustraction refuse le débit.
  2. Les lectures atomiques réservent chacune 45 unités.
  3. Les deux débits peuvent laisser un solde de −20.
  4. Seul le premier appelant peut lire 70.
Afficher la réponseMasquer la réponse
Réponse : C
Les deux lectures atomiques peuvent lire 70 avant l’un ou l’autre débit. Les deux soustractions atomiques produisent ensuite 25 et −20, car la soustraction seule ne répète pas la décision antérieure sur les fonds disponibles. Les accès individuellement atomiques empêchent les mises à jour partielles, mais ne combinent pas le test et le débit en une seule opération conditionnelle.

Question 6 sur 10

Disponible vaut (1, 1). A a pour Allocation (1, 0) et pour Max (2, 2) ; B a pour Allocation (0, 1) et pour Max (1, 2). Quelle est la conclusion du test de sûreté du banquier ?
  1. État sûr ; B peut terminer avant A.
  2. État sûr ; A peut terminer avant B.
  3. État non sûr ; aucune demande restante ne peut initialement être satisfaite.
  4. État non sûr ; le maximum total de A dépasse Disponible.
Afficher la réponseMasquer la réponse
Réponse : A
Le Besoin de A est (1, 2) : il ne peut donc pas commencer avec Travail (1, 1). Le Besoin de B, (1, 1), peut être satisfait et B rend son Allocation (0, 1), ce qui donne Travail (1, 2) ; A peut alors terminer. Comparer le maximum total de A aux ressources initialement disponibles compte à tort les ressources qu’il détient déjà et ignore la libération ultérieure par B.

Question 7 sur 10

Une attente sur un mutex avec délai renvoie ETIMEDOUT. Qu’est-ce que cela établit ?
  1. Le détenteur se trouve dans un cycle de verrous.
  2. Le mutex a été libéré.
  3. L’état protégé est corrompu.
  4. L’attente a dépassé son échéance.
Afficher la réponseMasquer la réponse
Réponse : D
La valeur de retour établit que l’acquisition demandée ne s’est pas terminée dans le délai fixé. Un détenteur simplement lent ou un délai d’ordonnancement peut produire le même résultat. L’expiration d’un délai ne permet donc pas à elle seule d’identifier un cycle d’interblocage ni d’indiquer au programme que ses données protégées ont été corrompues.

Question 8 sur 10

Un serveur détient 30 000 sockets pour la plupart inactives et effectue peu de calculs par message. Pourquoi une boucle d’événements peut-elle utiliser moins de mémoire par connexion qu’un fil d’exécution dédié par socket ?
  1. Les connexions n’exigent aucun état pendant leur inactivité.
  2. Les connexions peuvent utiliser de petits enregistrements d’état sauvegardé.
  3. Chaque gestionnaire d’événement reçoit un cœur de processeur distinct.
  4. Toutes les sockets partagent un descripteur réseau réutilisable.
Afficher la réponseMasquer la réponse
Réponse : B
Une conception événementielle peut stocker un enregistrement compact de connexion au lieu d’une pile de fil d’exécution dédiée pour chaque socket essentiellement inactive. Les connexions inactives ont toujours besoin d’un état, comme leurs descripteurs et leur étape dans le protocole. L’économie de mémoire vient de la représentation de cet état, et non de sa suppression ou d’une exécution multicœur automatique.

Question 9 sur 10

Un fil en attente lit done comme faux sans le mutex. Le travailleur prend ensuite le verrou, rend done vrai, signale alors qu’aucun fil n’attend et libère le verrou. Le fil en attente prend alors le verrou et appelle l’opération d’attente sans vérifier de nouveau. Quelle défaillance de dépendance s’est produite ?
  1. Un réveil intempestif avant le signalement du travailleur.
  2. Une nouvelle vérification Mesa sûre après l’acquisition du mutex.
  3. Un signalement mémorisé en attente du prochain appelant.
  4. Un réveil perdu après une vérification non protégée.
Afficher la réponseMasquer la réponse
Réponse : D
Le signalement du travailleur n’a trouvé aucun fil en attente et l’attente ultérieure a ignoré l’état désormais vrai. Le fil peut rester endormi, car aucun autre signalement n’est garanti. Acquérir le mutex seulement après la vérification ne corrige pas cet intervalle ; vérifie l’état en le détenant et conserve le verrou jusqu’à l’appel d’attente.

Question 10 sur 10

Dans un graphe d’attente de ressources à instance unique, U → V, V → W, W → V, et X n’a aucune arête sortante. Quels fils d’exécution forment le cycle de dépendances ?
  1. V et W.
  2. U, V et W.
  3. U et X.
  4. V, W et X.
Afficher la réponseMasquer la réponse
Réponse : A
V attend W et W attend V : ces deux fils forment donc le cycle. U ne peut pas continuer tant que sa dépendance reste bloquée, mais aucun chemin ne mène du cycle à U. X n’a aucune attente sortante : l’inclure reviendrait donc à confondre des fils sans rapport ou dépendants avec les membres du cycle.
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

Un service dispose de sept identificateurs de périphérique identiques et libres. Quelle valeur initiale du sémaphore permet d’emprunter tous les identificateurs disponibles ?
  1. 0
  2. 1
  3. 7
  4. 8
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Quelle boucle doit entourer une attente sur une variable de condition POSIX lorsqu’un consommateur a besoin d’une file non vide ?
  1. while (count == 0)
  2. if (count == 0)
  3. while (count > 0)
  4. if (count > 0)
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Quel compte initial du sémaphore laisse entrer exactement un fil d’exécution dans une section critique avant une opération V ?
  1. 0
  2. 2
  3. −1
  4. 1
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Une machine dispose de 12 Go de RAM. D’autres travaux nécessitent 3 Go et chaque travailleur admis a besoin de 750 Mo. Avec les unités décimales du SI, combien de travailleurs peuvent tenir dans la mémoire restante ?
  1. 16
  2. 12
  3. 9
  4. 15
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Deux fils d’exécution réservent chacun quatre places. Il reste six places et les deux vérifications de disponibilité se terminent avant l’une ou l’autre réservation. Quelle délimitation de section critique empêche la surréservation ?
  1. Une zone verrouillée pour vérifier, puis une autre pour réserver.
  2. Une seule zone verrouillée pour vérifier et réserver.
  3. Un verrou de lecture pour toute la réservation de chaque fil.
  4. Un mutex privé distinct pour chaque réservation.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Un fil d’exécution chargé d’un rapport a besoin d’un total final écrit par un travailleur juste avant sa terminaison. Quelle opération garantit cette terminaison avant que le rapport lise le total ?
  1. Se mettre brièvement en sommeil avant la lecture.
  2. Céder le processeur avant la lecture.
  3. Verrouiller uniquement pendant la lecture.
  4. Effectuer la jonction du travailleur avant la lecture.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Le fil d’exécution H détient M et attend N. Le fil J détient N et attend M. Les deux verrous sont exclusifs et ne peuvent pas être retirés de force. Comment décrire ces deux fils ?
  1. Un ensemble en interblocage.
  2. Un groupe en famine.
  3. Une paire en verrouillage actif.
  4. Une paire prête à s’exécuter.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Un transfert verrouille des objets de compte dotés de clés uniques. Quelle règle d’acquisition empêche les transferts en sens opposés de créer une attente circulaire ?
  1. Acquérir le compte source avant le compte destination.
  2. Acquérir le compte destination avant le compte source.
  3. Acquérir les deux comptes par ordre croissant de clé.
  4. Acquérir d’abord le compte dont le solde est le plus faible.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Une boucle surveille les descripteurs 2, 8 et 14 avec select. Quelle valeur doit-elle passer pour nfds ?
  1. 3
  2. 14
  3. 15
  4. 16
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Le gestionnaire A soumet une lecture pour la socket client 40. Le gestionnaire B soumet ensuite une autre lecture pour la socket 41. Les deux gestionnaires de fin utilisent une destination globale, qui vaut maintenant 41. La lecture de A se termine en premier. Où la réponse de A est-elle dirigée et quelle correction préserve les deux clients ?
  1. Vers 40 ; continuer à utiliser la destination globale.
  2. Vers 41 ; sauvegarder la destination par requête.
  3. Vers 40 ; reconstruire l’ensemble select avant de répondre.
  4. Vers 41 ; ne conserver qu’un compte des requêtes en cours.
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

Un allocateur partagé n’a aucune mémoire libre. Un fil en attente a besoin de 180 octets et un autre de 40. Une libération ajoute 90 octets et ne signale que le fil qui attend 180 octets ; aucun autre changement ne suit. Comment modifier la politique de notification pour permettre à un fil dont la demande peut être satisfaite de continuer ?
  1. Effectuer une diffusion après la libération.
  2. Signaler une fois que 180 octets sont disponibles.
  3. Se passer du mutex pour la mise à jour.
  4. Remplacer les deux boucles while par if.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Un tampon de capacité deux utilisant des variables de condition contient un élément. Chacun de deux producteurs vérifie la présence d’une place sous le mutex, le libère, puis le reprend plus tard uniquement pour effectuer l’insertion. Les deux vérifications se terminent avant l’une ou l’autre insertion. Quel état les deux insertions peuvent-elles produire ?
  1. Compteur à 2 ; la seconde vérification réserve la place.
  2. Compteur à 1 ; l’une des insertions sérialisées est ignorée.
  3. Compteur à 3 ; l’intervalle laisse les deux vérifications admettre un élément.
  4. Compteur à 0 ; le retour de l’indice au début vide le tampon.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Aucun fil d’exécution n’attend sur un sémaphore de fin lorsque son travailleur effectue une opération V. Que laisse cette opération pour une opération P ultérieure réussie ?
  1. Un propriétaire de mutex mémorisé.
  2. Une notification perdue.
  3. Un fil en attente bloqué.
  4. Un jeton conservé.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un verrou de lecture protège un cache. Un lecteur constate qu’une entrée manque, libère l’accès en lecture et attend un accès en écriture. Un autre rédacteur remplit l’entrée avant qu’il n’entre. Que doit faire le lecteur initial après avoir acquis l’accès en écriture ?
  1. Insérer immédiatement à partir du constat antérieur.
  2. Vérifier de nouveau si l’entrée manque encore.
  3. Reprendre l’accès en lecture en conservant l’accès en écriture.
  4. Libérer l’accès en écriture sans examiner le cache.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

L’initialisation et la première utilisation acquièrent chacune le même mutex, mais la première utilisation voit parfois un enregistrement non initialisé. Quelle garantie manque ?
  1. Un accès exclusif pendant chaque opération.
  2. Une initialisation terminée avant l’utilisation.
  3. Un verrou privé différent pour chaque opération.
  4. Un délai fixe après chaque libération du mutex.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Des tâches acquièrent l’ensemble de leurs ressources en détenant un mutex global d’acquisition, puis libèrent ce mutex avant de travailler. Chaque chemin suit cette règle et la libération des ressources ne nécessite jamais le mutex d’acquisition. Pourquoi des acquisitions partielles concurrentes ne peuvent-elles pas former un cycle ?
  1. Une seule séquence d’acquisition inachevée peut exister.
  2. Aucun fil ne peut jamais détenir une ressource en attendant.
  3. Toutes les ressources deviennent préemptibles au début du travail.
  4. Le processeur ne peut pas préempter un fil pendant l’acquisition.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Il existe 9 ressources identiques. A en détient 3 avec Max égal à 7 ; B en détient 2 avec Max égal à 4. A en demande 3 de plus. Selon l’algorithme du banquier, faut-il accorder cette demande ?
  1. Non ; l’état obtenu n’a aucune séquence sûre.
  2. Oui ; B peut terminer avant A.
  3. Non ; la demande dépasse la demande restante de A.
  4. Oui ; A peut terminer avant B.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Une socket de flux disponible en lecture renvoie 0 lors d’une lecture réussie. Quelle condition d’entrée le gestionnaire doit-il traiter ?
  1. Un résultat temporaire indiquant que l’opération bloquerait.
  2. Un octet reçu de valeur zéro.
  3. Une fin d’entrée provenant de l’autre extrémité.
  4. Une fin de lecture disque en attente.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Un gestionnaire place une lecture disque asynchrone en file, libère son tampon de destination et revient. La requête est toujours en cours. Que faut-il changer pour que le traitement de fin puisse utiliser le résultat en toute sécurité ?
  1. Ne conserver que la valeur du descripteur jusqu’au retour.
  2. Lire immédiatement puisque la soumission a déjà réussi.
  3. Déplacer la libération du tampon avant la soumission.
  4. Garder le tampon valide jusqu’à la fin de l’entrée-sortie.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Une boucle vérifie un quota partagé, attend une approbation externe, puis décrémente le quota. Deux tâches peuvent chacune réussir la vérification avant que l’une ou l’autre ne reprenne. Quelle modification empêche les deux de réclamer la dernière unité ?
  1. Rendre les lectures du quota atomiques mais garder la décision scindée.
  2. Verrouiller uniquement la décrémentation après l’approbation.
  3. Réserver atomiquement une unité de quota avant d’attendre.
  4. Réserver une unité après l’approbation selon la vérification antérieure.
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

Un fil qui attend une condition détient le mutex M pour son prédicat et détient aussi le verrou L. L’attente libère M. Le seul fil capable de rendre le prédicat vrai doit d’abord acquérir L. Qu’est-ce qui empêche la progression et quelle dépendance doit changer ?
  1. Le fil qui signale doit acquérir M deux fois avant de modifier l’état.
  2. Le fil en attente doit garder M verrouillé pendant tout son sommeil.
  3. Le fil en attente doit abandonner le prédicat et attendre une fois.
  4. Le fil en attente doit cesser de détenir L pendant cette attente.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Quatre consommateurs traitent une file FIFO. La jonction de tous les producteurs a maintenant été effectuée, des éléments réels peuvent rester dans la file et un marqueur d’arrêt réservé fait partir un consommateur. Combien de marqueurs le coordinateur doit-il placer dans la file ?
  1. 1
  2. 4
  3. 3
  4. 5
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Un ensemble de ressources commence avec trois jetons. Un emprunteur effectue une opération P réussie, rend plus tard sa ressource et effectue accidentellement deux opérations V. Une fois tout le travail terminé, combien d’emprunteurs peuvent franchir une opération P avant que quiconque effectue une nouvelle opération V ?
  1. 4
  2. 2
  3. 3
  4. 5
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un tampon vide de capacité deux utilise des sémaphores. P1 et P2 effectuent chacun une opération P réussie sur empty, puis P1 insère sous la protection du mutex et le libère, mais n’a pas effectué d’opération V sur full. P2 n’a pas inséré. Un consommateur effectue alors une opération P sur full. Que se passe-t-il ?
  1. Il retire l’élément de P1 avant que P2 insère.
  2. Il se bloque jusqu’à ce que P2 termine son insertion.
  3. Il se bloque jusqu’à la mise à disposition d’un jeton d’élément.
  4. Il retire l’élément de P1 dès que le mutex est libre.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un programmeur veut que l’incrément d’un compteur partagé soit une mise à jour indivisible sans utiliser de mutex. Quelle opération atomique réalise directement cette mise à jour ?
  1. Une lecture atomique seule.
  2. Une écriture atomique seule.
  3. Un signalement de condition.
  4. Une lecture et addition.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Cinq philosophes à table utilisent un sémaphore initialisé à 4. Chacun effectue une opération P dessus, prend une fourchette à gauche, effectue une opération V sur le sémaphore, puis essaie de prendre la fourchette de droite. Les cinq peuvent-ils se bloquer en détenant chacun une fourchette ?
  1. Non ; le compte initial limite durablement les détenteurs de fourchettes à quatre.
  2. Oui ; rendre les jetons trop tôt permet aux cinq d’atteindre le cycle des fourchettes.
  3. Non ; chaque opération V rend atomiquement la fourchette gauche du philosophe.
  4. Oui ; mais uniquement si le compte initial du sémaphore dépasse quatre.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Disponible vaut (1, 1). P détient (1, 0), Max (3, 1) ; Q détient (1, 1), Max (2, 2). P demande (0, 1). L’algorithme du banquier doit-il accorder cette demande ?
  1. Oui ; P peut terminer avec les ressources restantes.
  2. Non ; l’état provisoire n’a aucune séquence sûre.
  3. Oui ; Q peut terminer avec les ressources restantes.
  4. Non ; P demande plus que sa demande restante.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Un détecteur pour plusieurs instances constate que Disponible vaut (0, 0). P détient (1, 0) et demande (0, 1) ; Q détient (0, 1) et demande (1, 0). R ne détient rien et demande (1, 0). Quels détenteurs entretiennent l’interblocage de ressources ?
  1. P, Q et R.
  2. P et R.
  3. P et Q.
  4. Q et R.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Une boucle select surveille initialement les sockets de flux 6 et 10. Seule la socket 6 est renvoyée comme disponible. La boucle réutilise cet ensemble renvoyé sans le reconstruire, tandis que la socket 10 reçoit des données et que les lectures sur la socket 6 renvoient maintenant 0. Quelles deux défaillances peuvent en résulter ?
  1. La socket 10 reste surveillée ; la socket 6 fournit un octet de données nul.
  2. La socket 10 devient disponible en écriture ; la socket 6 attend une lecture disque.
  3. La socket 10 est omise ; la socket 6 peut être retraitée en fin d’entrée.
  4. La socket 10 remplace la 6 dans l’ensemble ; select ferme la socket 6.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Quarante requêtes sont en file sur une boucle d’événements. Chaque gestionnaire effectue une lecture disque bloquante de 6 ms avant de revenir, les autres travaux étant négligeables. Combien de temps la dernière requête attend-elle avant le début de sa lecture ?
  1. 234 ms
  2. 240 ms
  3. 6 ms
  4. 39 ms
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

Un fil d’exécution Java appelle notifyAll sur un objet en dehors de toute zone synchronized et sans détenir son moniteur par ailleurs. Que se passe-t-il ?
  1. Il conserve une notification pour les futurs fils en attente.
  2. Il déclenche IllegalMonitorStateException.
  3. Il transmet les verrous de l’appelant aux fils en attente.
  4. Il attend que le moniteur soit libre.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Un sémaphore est construit avec un mutex, une variable de condition et une valeur positive ou nulle. L’opération V incrémente et signale sous la protection du mutex. L’opération P vérifie la valeur avec if, attend si elle vaut zéro, puis décrémente. Comment peut-elle tout de même échouer ?
  1. Le mutex laisse deux fils modifier la valeur simultanément.
  2. Le sémaphore ne peut mémoriser aucune opération V sans fil en attente.
  3. Un fil revenant de l’attente peut décrémenter une valeur nulle.
  4. Une opération V décrémente toujours la valeur sans fil en attente.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Un consommateur détient un mutex de tampon pendant qu’il attend sur le sémaphore des éléments. Le tampon est vide. Une correction proposée consiste à faire effectuer au producteur une opération V sur le sémaphore des éléments avant l’insertion, afin qu’il puisse réveiller le consommateur sans le mutex. Pourquoi cette correction est-elle dangereuse ?
  1. Elle permet un retrait avant l’insertion du producteur.
  2. Elle permet une insertion avant la réservation d’une place.
  3. Elle rend un jeton de place libre avant la fin du retrait.
  4. Elle publie deux fois le même jeton d’élément inséré.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Dans la conception de base avec compteur de lecteurs, le premier lecteur peut détenir le verrou du compteur en attendant writelock. Pourquoi le rédacteur actif peut-il tout de même terminer, contrairement à un producteur bloqué par le mutex de tampon d’un consommateur ?
  1. Le lecteur en attente libère le verrou du compteur dans sem_wait.
  2. Le rédacteur détient déjà lui-même le verrou du compteur de lecteurs.
  3. L’opération V du rédacteur attend que le compteur de lecteurs atteigne zéro.
  4. Le rédacteur peut effectuer une opération V sur writelock sans le verrou du compteur.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un travailleur lit un pointeur partagé avant que le créateur ait fini de l’affecter. De quel type de bogue de dépendance s’agit-il ?
  1. Une violation d’ordre.
  2. Un interblocage de ressources.
  3. Une famine des rédacteurs.
  4. Un verrouillage actif de nouvelles tentatives.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Deux boucles de mise à jour acquièrent chacune un verrou, échouent à prendre l’autre par une tentative non bloquante, libèrent leur verrou et réessaient ensemble indéfiniment. Une correction proposée consiste à garder le premier verrou pendant une attente normale du second. Quel risque ce changement présente-t-il ?
  1. Il conserve le verrouillage actif, car les deux fils réessaient toujours.
  2. Il garantit qu’un fil termine tandis que l’autre peut subir une famine.
  3. Il remplace le verrouillage actif par un interblocage possible.
  4. Il sérialise les deux acquisitions sous un premier verrou commun.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Disponible vaut (1, 1). P détient (1, 0), Max (2, 1) ; Q détient (0, 2), Max (1, 3) ; R détient (1, 0), Max (3, 2). Quelle séquence sûre permet aux trois de terminer ?
  1. R, P, Q.
  2. P, R, Q.
  3. Q, R, P.
  4. P, Q, R.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Disponible vaut (0, 0). A détient (1, 0), demande actuellement (0, 0) et a pour Besoin maximal restant (0, 2). B détient (0, 2) et demande actuellement (1, 0). La détection fondée sur les demandes actuelles trouve-t-elle un interblocage ?
  1. Oui ; aucun Besoin maximal ne peut être satisfait par Disponible initial.
  2. Non ; A libère (1 ; 0), ce qui permet de satisfaire la demande de B.
  3. Oui ; la demande actuelle de B ne peut pas être satisfaite immédiatement.
  4. Non ; les deux demandes peuvent déjà être satisfaites par Disponible initial.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Un développeur remplace les parcours répétés des requêtes d’entrée-sortie en cours par une file de complétion. Quel travail la boucle peut-elle maintenant lancer directement ?
  1. Les réponses finales de chaque nouvelle requête soumise.
  2. La suite du travail pour les opérations signalées comme terminées.
  3. Les lectures de chaque fichier ordinaire indiqué comme disponible par select.
  4. L’exécution parallèle de tous les gestionnaires sur un fil de boucle.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Un tampon circulaire de capacité deux commence vide. Insère 31, insère 32, retire une fois, puis insère 33. Les deux indices valent maintenant 1. Quels sont le nombre d’éléments présents et la valeur renvoyée par le prochain retrait ?
  1. Compteur à 1 ; prochaine valeur 32.
  2. Compteur à 2 ; prochaine valeur 33.
  3. Compteur à 0 ; le retrait doit attendre.
  4. Compteur à 2 ; prochaine valeur 32.
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

90 Mots-Clés, chacun avec sa définition

A

Absence de préemption
L’absence de préemption signifie qu’une ressource détenue ne peut pas être retirée de force à son détenteur et doit être libérée selon son protocole.
Un fil d’exécution qui attend un mutex ordinaire ne peut pas simplement retirer le verrou à son propriétaire actuel.
Admission équitable
L’admission équitable accorde l’accès selon une politique qui empêche une catégorie d’arrivants continuels de passer indéfiniment devant un fil en attente.
Une politique équitable lecteurs-rédacteurs empêche des lecteurs arrivés plus tard de passer indéfiniment devant un rédacteur arrivé plus tôt.
Algorithme du banquier
L’algorithme du banquier n’accorde une demande de ressources que si elle respecte les demandes restantes déclarées et préserve une séquence sûre de terminaison.
Un allocateur accorde provisoirement une demande, teste l’état obtenu et retarde une attribution non sûre.
Attente circulaire
Une attente circulaire est une chaîne fermée de fils d’exécution dans laquelle chacun attend une ressource détenue par le fil suivant de la chaîne.
Trois fils d’exécution dans lesquels A attend B, B attend C et C attend A ferment le cycle de dépendances.

B

Bloc de contrôle d’entrée-sortie asynchrone
Un bloc de contrôle d’entrée-sortie asynchrone décrit une requête d’entrée-sortie asynchrone POSIX, notamment son descripteur, son déplacement dans le fichier, son tampon et le nombre d’octets demandé.
La structure aiocb et son tampon de destination restent valides tant que la lecture placée en file est en cours.
Blocage implicite
Un blocage implicite retarde un fil d’exécution par une opération qui paraît immédiate, comme l’accès à une page qui doit être lue sur disque.
Un gestionnaire se bloque sur un défaut de page même s’il n’appelle aucune lecture explicitement bloquante.
Boucle d’événements
Une boucle d’événements attend des événements et lance de courts gestionnaires, permettant à un fil d’exécution de coordonner de nombreuses tâches dont les périodes d’attente se chevauchent.
Un serveur traite les données d’une socket disponible, puis revient servir une autre connexion.

C

Chien de garde
Un chien de garde déclenche un rétablissement lorsque le logiciel ne signale pas sa progression dans un délai fixé, sans diagnostiquer la cause de manière certaine.
Un contrôleur embarqué se réinitialise après que son logiciel a cessé d’envoyer les comptes rendus de progression attendus.
Comparaison et échange
La comparaison et échange ne remplace atomiquement la valeur d’un emplacement que lorsque sa valeur actuelle correspond à la valeur attendue fournie par l’appelant.
Un fil qui met à jour un compteur réessaie lorsqu’un autre fil d’exécution modifie l’ancienne valeur avant la réussite de sa comparaison et échange.
Concurrence hybride
La concurrence hybride combine une boucle d’événements et des travailleurs qui effectuent des opérations bloquantes ou exigeantes en calcul hors du fil d’exécution de la boucle.
Une boucle réseau envoie le traitement d’images à des travailleurs puis traite les notifications de fin.
Condition couvrante
Une condition couvrante utilise une notification générale pour atteindre chaque fil en attente dont la condition plus précise pourrait désormais être satisfaite.
Lorsqu’un allocateur ayant des demandeurs en attente de 10 et de 100 octets libère 50 octets, il réveille les deux pour que chacun puisse vérifier de nouveau.
Continuation
Une continuation stocke l’état nécessaire pour reprendre une tâche après le retour d’un gestionnaire qui attendait une opération asynchrone.
La requête de lecture enregistre la socket client qui doit recevoir ses données.
Contrôle d’admission
Le contrôle d’admission décide si davantage de travail peut entrer dans une zone aux ressources limitées et le retarde lorsque la capacité autorisée est utilisée.
Un travailleur attend un jeton de traitement avant d’allouer son grand tampon temporaire.
Conversion d’un verrou de lecture en verrou d’écriture
Une conversion d’un verrou de lecture en verrou d’écriture transforme l’accès en lecture en accès exclusif en écriture ; dans la conception de base, l’accès en lecture doit d’abord être libéré.
Après avoir acquis l’accès en écriture, un fil qui met à jour le cache vérifie de nouveau si l’entrée doit encore être modifiée.

D

Détection des interblocages
La détection des interblocages recherche dans les dépendances d’attente ou les demandes de ressources actuelles un ensemble existant qui ne peut pas progresser.
Une recherche de cycle trouve trois détenteurs de mutex bloqués les uns sur les autres.
Diffusion
Une diffusion réveille tous les fils actuellement en attente sur une variable de condition ; chacun doit reprendre le mutex et tester son propre prédicat.
Un allocateur effectue une diffusion après une libération, car les fils en attente demandent des quantités de mémoire différentes.
Disponibilité pour une entrée-sortie
La disponibilité pour une entrée-sortie indique qu’une opération d’entrée-sortie peut être tentée pour un descripteur ; elle ne transfère pas les données dans le programme.
Une socket disponible en lecture peut renvoyer des données ou signaler une fin d’entrée lors de la lecture.

E

Ensemble de descripteurs
Un ensemble de descripteurs représente des numéros de descripteurs de fichier sélectionnés pour être surveillés, avec des bits indiquant leur appartenance ou la disponibilité renvoyée.
Le serveur reconstruit son ensemble de lecture, car select() le remplace par le sous-ensemble disponible.
Entrées-sorties asynchrones
Les entrées-sorties asynchrones séparent la soumission d’une opération de la récupération de son résultat final, permettant à l’appelant de traiter d’autres travaux entre-temps.
Le gestionnaire soumet une lecture de fichier et envoie la réponse après avoir reçu la notification de fin.
Entrées-sorties non bloquantes
Les entrées-sorties non bloquantes rendent le contrôle lorsqu’une opération ne peut pas continuer immédiatement, afin que le fil d’exécution appelant puisse traiter un autre travail.
Une lecture de socket renvoie EAGAIN et le gestionnaire revient à la boucle d’événements.
Entrelacement
Un entrelacement est un ordre des étapes de différents fils d’exécution qui conserve l’ordre propre à chaque fil tout en mélangeant leurs accès.
Placer la mise à zéro d’un pointeur entre sa vérification et son utilisation par un autre fil d’exécution révèle l’ordonnancement dangereux.
epoll
epoll est une interface événementielle de Linux qui conserve dans le noyau un ensemble à surveiller et signale les événements disponibles pour les descripteurs enregistrés.
Un serveur peut surveiller de nombreuses connexions sans soumettre de nouveau toute la liste surveillée à chaque fois.
Exclusion mutuelle
L’exclusion mutuelle n’autorise qu’un seul fil d’exécution à la fois à détenir une ressource ou à exécuter une opération protégée sur l’état partagé.
Un mutex exclusif ne peut pas être accordé à un second fil d’exécution tant que son propriétaire le détient encore.
Expiration du délai d’attente
L’expiration du délai d’attente met fin à une attente après une échéance, révélant un délai excessif sans distinguer un interblocage permanent d’une progression lente.
L’expiration d’un délai d’attente sur un mutex peut résulter aussi bien d’un système surchargé que d’un cycle de verrous.

É

État non sûr
Un état non sûr ne possède aucune séquence de terminaison garantie pour les demandes maximales, même si le comportement futur réel peut encore éviter l’interblocage.
Un fil d’exécution pourrait libérer des ressources plus tôt au lieu de formuler toute sa demande restante.
État sûr
Un état sûr possède au moins un ordre de terminaison qui peut satisfaire la demande maximale restante de chaque fil d’exécution grâce aux ressources disponibles et libérées.
Un fil d’exécution peut terminer en premier et libérer assez de ressources pour qu’un second puisse ensuite terminer.
Évitement des interblocages
L’évitement des interblocages vérifie les attributions de ressources proposées à partir d’informations préalables sur les demandes et retarde celles qui laisseraient le système dans un état non sûr.
Une ressource libre peut ne pas être accordée si son attribution supprimerait toutes les séquences sûres de terminaison.

F

Famine
La famine reporte indéfiniment un fil d’exécution tandis que d’autres poursuivent un travail utile, même si aucun cycle de dépendances bloquées ne les piège tous.
Un rédacteur continue à attendre tandis qu’un flux ininterrompu de lecteurs dont les accès se chevauchent utilise la ressource.
Famine des rédacteurs
La famine des rédacteurs est le report indéfini d’un rédacteur en attente tandis que des lecteurs continuent à entrer et à progresser.
Un verrou qui ne fait que compter les lecteurs ne donne aucun tour à un rédacteur si les accès des nouveaux lecteurs se chevauchent continuellement.
File de complétion
Une file de complétion fournit des enregistrements pour les opérations asynchrones terminées, permettant à un programme de récupérer les résultats et de lancer la suite du travail.
La boucle lit un enregistrement de complétion au lieu de vérifier à répétition chaque lecture en cours.

G

Gestion manuelle de la pile
La gestion manuelle de la pile préserve explicitement l’état local d’une tâche entre les retours des gestionnaires, au lieu de s’appuyer sur une seule pile d’appels suspendue.
Un enregistrement de réponse conserve la position et la destination de la requête jusqu’à la fin de la lecture.
Graphe d’attente
Un graphe d’attente représente les fils d’exécution par des nœuds et les dépendances d’attente par des arêtes orientées du fil en attente vers un détenteur de ressource.
Une flèche de T1 vers T2 signifie que T1 a besoin d’une ressource que T2 détient actuellement.

I

Indicateur de disponibilité
Un indicateur de disponibilité enregistre la fin de l’initialisation sous synchronisation, ce qui permet aux utilisateurs d’attendre que l’état nécessaire existe.
Le créateur ne positionne ready qu’après avoir affecté l’enregistrement que le travailleur utilisera.
Interblocage
Un interblocage piège un ensemble de fils d’exécution bloqués, car chacun a besoin d’une action ou d’une ressource que seul un autre membre piégé peut fournir.
T1 détient A et attend B tandis que T2 détient B et attend A.
io_uring
io_uring est une interface Linux dotée de files de soumission et de complétion pour les opérations asynchrones, qui permet de traiter les entrées-sorties en fonction de leur fin.
Un serveur soumet des requêtes puis lance les continuations à partir de leurs enregistrements de complétion.

J

Jeton
Un jeton représente une autorisation d’entrée ou une unité de ressource disponible qu’une opération P réussie sur un sémaphore consomme et qu’une opération V fournit.
Un sémaphore possédant trois jetons inutilisés peut admettre trois fils d’exécution sans les bloquer.
Jonction
Une jonction attend la terminaison d’un fil d’exécution précis avant que le fil qui effectue la jonction ne poursuive le travail qui nécessite cette fin.
Le fil d’exécution chargé du rapport effectue la jonction du travailleur chargé de la somme avant de lire son total final.

K

kqueue
kqueue est une interface de notification d’événements sur FreeBSD et macOS qui maintient les événements à surveiller enregistrés et renvoie les événements signalés.
Un serveur enregistre les événements de socket à surveiller avec kqueue au lieu de reconstruire les ensembles complets de descripteurs select.

L

Lecture et addition
La lecture et addition est une opération atomique de lecture-modification-écriture qui ajoute une quantité et renvoie l’ancienne valeur sans mise à jour intermédiaire.
Une lecture et addition atomique empêche des incréments concurrents d’un compteur de s’écraser mutuellement.
Libération et mise en attente atomiques
La libération et mise en attente atomiques font de la libération du mutex et de l’entrée en attente sur une condition une seule transition indivisible vis-à-vis des fils d’exécution concurrents.
Le fil fils ne peut pas signaler dans un intervalle après que le fil père a libéré le verrou mais avant qu’il ne commence à attendre.
Limitation de la concurrence
La limitation de la concurrence limite l’entrée simultanée dans une phase coûteuse afin que la demande combinée reste dans un budget de ressources choisi.
Douze travailleurs simultanés utilisant chacun 500 Mo consomment 6 000 Mo pendant leur phase gourmande en mémoire.

M

Marqueur de fin de données
Un marqueur de fin de données est un élément réservé de la file qui indique à un consommateur de s’arrêter après le traitement de tous les éléments réels qui le précèdent.
Un coordinateur effectue la jonction de chaque producteur avant d’insérer un marqueur d’arrêt par consommateur.
Mise à jour perdue
Une mise à jour perdue se produit lorsque des opérations de lecture-modification-écriture qui se chevauchent s’écrasent mutuellement, de sorte qu’une modification prévue disparaît.
Deux fils d’exécution lisent un compteur à 8 et écrivent chacun 9, alors que deux incréments devraient donner 10.
Modèle à compteur signé
Le modèle à compteur signé représente les fils bloqués en attente sur un sémaphore par des valeurs négatives, tout en conservant le même comportement des opérations P et V pour les fils d’exécution.
Une valeur de trace de −2 représente deux fils bloqués en attente, et non un compte négatif de jetons Linux.
Moniteur
Un moniteur regroupe un état partagé avec des opérations qui n’admettent qu’un fil d’exécution à la fois, ainsi qu’un mécanisme d’attente fondé sur des conditions.
Un bloc synchronized de Java détient le moniteur d’objet nécessaire aux appels de wait().
Mutex du tampon
Un mutex du tampon sérialise les modifications du tableau et des indices du tampon, indépendamment des sémaphores qui comptent les ressources disponibles.
Deux producteurs disposant de jetons de place valides mettent toujours à jour l’indice d’écriture du tampon, fill_ptr, sous la protection du même mutex.

O

Opération P sur un sémaphore
Une opération P sur un sémaphore acquiert atomiquement un jeton et se bloque lorsqu’aucun jeton n’est disponible, jusqu’à ce qu’une autre opération en fournisse un.
Un emprunteur effectue une opération P avant de prendre un identificateur afin de ne pas dépasser la limite d’admission de l’ensemble.
Opération V sur un sémaphore
Une opération V sur un sémaphore fournit atomiquement un jeton, permettant à un fil bloqué en attente de continuer ou conservant une disponibilité pour une opération P ultérieure.
Un travailleur effectue une opération V sur son sémaphore de fin même si son fil père n’a pas encore commencé à attendre.
Ordre d’acquisition des verrous
L’ordre d’acquisition des verrous attribue un ordre commun d’acquisition aux verrous et impose à chaque chemin de code concerné de le suivre.
Dans les deux sens d’un transfert, le compte au numéro le plus faible est verrouillé avant celui au numéro le plus élevé.

P

Parallélisme multicœur
Le parallélisme multicœur exécute des calculs simultanément sur plusieurs cœurs de processeur, ce qui nécessite davantage de travailleurs d’exécution qu’un seul fil de boucle.
Plusieurs travailleurs de traitement d’images peuvent calculer simultanément pendant qu’une boucle d’événements gère les sockets.
poll
poll() surveille un tableau d’entrées de descripteurs pour les événements d’entrée-sortie demandés, sans la restriction fixe de valeur des descripteurs de fd_set.
Une socket de numéro élevé peut être surveillée avec poll même lorsque sa valeur ne tient pas dans un ensemble select.
Pool de travailleurs
Un pool de travailleurs est un groupe borné de fils d’exécution ou de processus réutilisables qui effectuent les tâches soumises et renvoient leurs résultats.
libuv confie les opérations sur le système de fichiers à son pool de fils d’exécution pendant que la boucle traite les événements.
Possession du sémaphore
Un sémaphore n’a pas de propriétaire : tout fil d’exécution peut donc fournir un jeton par une opération V, même lorsqu’un autre fil a effectué l’opération P correspondante.
Un fil fils effectue une opération V sur un sémaphore de fin sur lequel son fil père attend.
Possession et attente
La possession et attente se produit lorsqu’un fil d’exécution conserve les ressources qu’il détient déjà tout en se bloquant pour obtenir d’autres ressources nécessaires.
Un transfert conserve le verrou d’un compte en attendant d’acquérir le verrou de l’autre compte.
Prédicat
Un prédicat est une condition vraie ou fausse portant sur l’état partagé, qu’un fil d’exécution vérifie avant de décider s’il peut continuer.
Le prédicat done != 0 permet à un fil père de vérifier si son fil fils a terminé.
Prévention des interblocages
La prévention des interblocages consiste à concevoir l’acquisition des ressources de sorte qu’au moins une condition nécessaire à l’interblocage ne puisse jamais se produire.
Un ordre commun des verrous de compte empêche les transferts en sens inverse de créer une attente circulaire.
Priorité aux lecteurs
La priorité aux lecteurs permet aux lecteurs qui arrivent de rejoindre un groupe de lecteurs actif même lorsqu’un rédacteur attend déjà l’accès.
Des lecteurs dont les accès se chevauchent peuvent maintenir le compteur de lecteurs au-dessus de zéro pendant qu’un rédacteur attend.
Priorité aux rédacteurs
La priorité aux rédacteurs bloque les nouveaux lecteurs lorsqu’un rédacteur attend, ce qui réduit le report des rédacteurs mais peut provoquer la famine des lecteurs.
Une consultation arrivante attend derrière le rédacteur du cache au lieu de rejoindre le groupe de lecteurs actuel.
Problème des producteurs et des consommateurs
Le problème des producteurs et des consommateurs consiste à coordonner des fils d’exécution qui ajoutent et retirent des éléments d’un tampon partagé de capacité limitée.
Un récepteur réseau produit des requêtes que des fils d’exécution travailleurs consomment dans une file bornée.

R

Rendez-vous
Un rendez-vous est un point de rencontre qu’aucun de deux fils d’exécution ne peut franchir avant que les deux aient annoncé leur arrivée.
Chaque fil d’exécution signale son arrivée par une opération V avant d’attendre le jeton d’arrivée de l’autre fil.
Réservation de ressource
Une réservation de ressource attribue une unité de ressource à une opération admise, même avant que cette opération ne mette à jour les données de gestion partagées.
Une opération P réussie d’un producteur sur le sémaphore des places libres réserve une place pendant qu’il attend le mutex du tampon.
Retour arrière
Un retour arrière restaure un point de contrôle valide antérieur afin que le travail puisse libérer ou récupérer des ressources sans conserver les modifications partielles.
Un système transactionnel annule et relance une transaction en interblocage grâce à son mécanisme de retour arrière.
Réveil intempestif
Un réveil intempestif est un retour d’une attente sur condition sans notification correspondante ; il exige la même nouvelle vérification du prédicat que tout réveil.
Un consommateur revient à l’attente dans sa boucle s’il se réveille mais trouve toujours le tampon vide.
Réveil perdu
Un réveil perdu se produit lorsqu’une notification nécessaire survient avant qu’un fil d’exécution ne commence à attendre, le laissant endormi sans notification ultérieure.
Lorsqu’un fil père vérifie un indicateur de fin en dehors du mutex, le fil fils peut terminer entre la vérification et l’attente.

S

select
select() surveille la disponibilité pour une entrée-sortie d’ensembles de descripteurs, les remplace par les sous-ensembles disponibles et limite l’examen aux numéros de descripteurs inférieurs à nfds.
Surveiller les descripteurs 5 et 12 nécessite nfds égal à 13 et la reconstruction des ensembles surveillés après chaque appel.
Sémantique de Hoare
La sémantique de Hoare transmet directement le verrou et l’exécution à un fil en attente notifié, en préservant la condition signalée au moment de cette transmission.
Le fil d’exécution notifié reprend avant qu’un autre entrant puisse consommer l’élément nouvellement disponible.
Sémantique de Mesa
La sémantique de Mesa rend un fil en attente notifié prêt à entrer en concurrence pour son verrou ; il doit donc vérifier de nouveau la condition après l’avoir repris.
Un second consommateur peut prendre l’unique élément avant que le premier consommateur notifié ne reprenne le mutex.
Sémaphore
Un sémaphore est un objet de synchronisation fondé sur un entier, doté d’opérations atomiques P et V qui acquièrent et fournissent des jetons.
Une opération V peut avoir lieu avant une opération P, car le sémaphore mémorise le jeton inutilisé.
Sémaphore à compteur
Un sémaphore à compteur commence avec le nombre d’unités identiques disponibles et limite le nombre d’utilisateurs qui peuvent les acquérir simultanément.
Un ensemble de connexions doté de quatre jetons fait attendre un cinquième emprunteur jusqu’à ce qu’une connexion soit rendue.
Sémaphore binaire
Un sémaphore binaire utilisé comme verrou commence avec un jeton et associe une acquisition avant une section critique à une libération après celle-ci.
Un second entrant se bloque jusqu’à ce que le premier rende le jeton de la section critique.
Sémaphore des éléments disponibles
Un sémaphore des éléments disponibles compte les éléments rendus disponibles dans le tampon que les consommateurs peuvent réserver avant de les retirer du tableau partagé.
Un tampon initialement vide donne zéro jeton à son sémaphore des éléments disponibles.
Sémaphore des places libres
Un sémaphore des places libres compte les jetons permettant de réserver les places actuellement disponibles dans un tampon avant que les producteurs n’y insèrent leurs éléments achevés.
Un tampon vide de capacité cinq initialise son sémaphore des places libres à cinq.
Séquence sûre
Une séquence sûre ordonne tous les fils d’exécution de sorte que chaque demande maximale restante puisse être satisfaite par les ressources disponibles après la terminaison des fils précédents.
Chaque étape d’une séquence proposée doit convenir à chaque type de ressource, et non seulement au nombre total.
Signalement
Un signalement notifie au moins un fil actuellement en attente sur une variable de condition lorsqu’il y en a, sans conserver de notification pour les arrivants ultérieurs.
Un fil fils signale après avoir positionné son indicateur de fin protégé, afin qu’un fil père en attente puisse le vérifier de nouveau.
Suspension
Une suspension met une fonction en pause jusqu’à la fin d’une opération attendue, tout en permettant à la boucle d’événements d’exécuter d’autres travaux prêts.
Une fonction asynchrone reprend après la fin de sa lecture, avec ses variables locales préservées.

T

Tampon borné
Un tampon borné est une file partagée de capacité fixe qui fait attendre les producteurs lorsqu’elle est pleine et les consommateurs lorsqu’elle est vide.
Un tampon de capacité trois ne peut pas accepter un quatrième élément avant qu’un consommateur en retire un.
Tampon circulaire
Un tampon circulaire stocke une file dans un tableau dont les indices de lecture et d’écriture reviennent de la dernière place à la première.
Après l’écriture à la place 2 d’un tableau de trois places, la prochaine écriture revient à la place 0.
Temporisation aléatoire
Une temporisation aléatoire ajoute un délai choisi au hasard avant une nouvelle tentative, ce qui réduit les collisions répétées entre des concurrents autrement synchronisés.
Des transferts qui réessaient attendent des intervalles différents afin de réduire la probabilité d’une nouvelle collision.
Tentative de verrouillage non bloquante
Une tentative de verrouillage non bloquante essaie d’acquérir un mutex et revient immédiatement lorsqu’il est indisponible, ce qui permet à l’appelant de libérer ses ressources et de réessayer.
L’échec d’une seconde tentative de verrouillage non bloquante conduit le transfert à libérer son premier verrou de compte.

V

Variable d’état
Une variable d’état enregistre un fait concernant un travail partagé, afin qu’un fil d’exécution puisse le vérifier même après le passage d’une notification antérieure.
Un indicateur de fin reste positionné lorsque le fil fils termine avant que le fil père ne commence à attendre.
Variable de condition
Une variable de condition fournit une file où les fils d’exécution se mettent en sommeil jusqu’à ce qu’un changement de l’état partagé leur permette de vérifier de nouveau leur condition.
Un consommateur attend sur une variable de condition lorsque la file de requêtes ne contient aucun travail.
Vecteur de travail
Un vecteur de travail représente la disponibilité simulée du test de sûreté ; il augmente de l’Allocation d’un fil d’exécution chaque fois que celui-ci est marqué comme terminé.
La terminaison d’un fil d’exécution qui détient (2, 1) fait passer Travail de (1, 3) à (3, 4).
Vecteur des besoins restants
Un vecteur des besoins restants enregistre la demande maximale restante d’un fil d’exécution pour chaque type de ressource, calculée en soustrayant l’Allocation actuelle de Max.
Un fil d’exécution avec Max (4, 3) et Allocation (1, 2) a pour Besoin (3, 1).
Vecteur des ressources disponibles
Un vecteur des ressources disponibles compte les unités actuellement non allouées de chaque type de ressource et constitue la valeur initiale de Travail pour un test de sûreté.
Soustrais toutes les unités détenues du total de chaque ressource avant de commencer la simulation de terminaison.
Vérification puis action
Une opération de vérification puis action teste l’état partagé, puis le modifie ou l’utilise d’une manière qui suppose que ce test reste valide.
La vérification du stock et sa réservation doivent rester groupées lorsque d’autres fils d’exécution peuvent vendre le même article.
Verrou lecteurs-rédacteurs
Un verrou lecteurs-rédacteurs permet à plusieurs lecteurs de partager l’accès aux données protégées ou à un seul rédacteur d’y accéder exclusivement.
Plusieurs consultations du cache peuvent se chevaucher, mais sa mise à jour nécessite un accès en écriture.
Verrouillage actif
Un verrouillage actif laisse les fils d’exécution s’exécuter et réagir les uns aux autres, tandis que leurs actions répétées ne parviennent pas à accomplir un travail utile.
Deux boucles de tentative de verrouillage non bloquante libèrent leurs verrous et réessaient ensemble à répétition sans qu’aucune n’obtienne les deux verrous.
Victime du rétablissement
Une victime du rétablissement est un travail sélectionné que le système interrompt ou ramène à un état antérieur pour libérer des ressources et résoudre un interblocage détecté.
Un processus ayant accompli peu de travail peut coûter moins cher à redémarrer qu’un processus presque terminé.
Violation d’atomicité
Une violation d’atomicité permet à un autre fil d’exécution d’interrompre des accès qui doivent se comporter comme une seule opération indivisible.
Deux zones de verrouillage distinctes autour d’une vérification de solde et d’un débit laissent toujours un intervalle dangereux.
Violation d’ordre
Une violation d’ordre se produit lorsqu’une opération s’exécute avant une opération antérieure nécessaire parce que leur dépendance n’est pas imposée.
Un travailleur lit la configuration avant que son chargeur ait fini de publier les valeurs.

À propos de cette unité

Par StudeliSignaler une erreur

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.

Commencer