Fils d’exécution, conditions de concurrence et verrous

Découvre comment les fils d’exécution permettent à un programme de faire plusieurs choses à la fois et pourquoi le partage de données entre eux présente des risques. Crée des fils d’exécution et effectue leur jonction avec l’interface POSIX en C, suis comment les conditions de concurrence entraînent des pertes de mises à jour et protège les sections critiques avec des mutex. Découvre comment les verrous sont construits à partir d’instructions matérielles atomiques comme le test et positionnement et la comparaison et échange, pourquoi les systèmes d’exploitation mettent les fils d’exécution en attente en sommeil et comment concevoir des compteurs, des listes, des files et des tables de hachage qui restent corrects et rapides en présence de nombreux fils d’exécution.

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

Ce que tu vas apprendre

  1. Décris comment les fils d’exécution d’un processus partagent un espace d’adressage tout en conservant chacun leur propre état d’exécution, et écris des programmes C qui créent des fils d’exécution, leur transmettent des données et attendent leur fin.
    • Compare les fils d’exécution et les processus
      Explique ce qui appartient à un fil d’exécution, ce qu’il partage avec les autres fils de son processus, pourquoi les programmes utilisent des fils d’exécution et en quoi le passage d’un fil à un autre diffère du passage d’un processus à un autre.
    • Crée des fils d’exécution, transmets-leur des données et effectue leur jonction avec pthreads
      Utilise l’API des fils d’exécution POSIX en C pour démarrer des fils, donner à chacun ses arguments, recueillir leurs résultats et t’assurer que le programme attend leur fin avant de se terminer.
  2. Suis comment l’entrelacement des instructions machine sur des données partagées produit des résultats incorrects, identifie les conditions de concurrence et les sections critiques dans le code et énonce les garanties qu’une solution correcte au problème des sections critiques doit offrir.
    • Suis comment des mises à jour non protégées entraînent la perte d’incréments
      Décompose une mise à jour partagée en étapes machine, suis les entrelacements qui entraînent des pertes de mises à jour et explique pourquoi ces bogues sont rares pendant les tests mais néanmoins réels.
    • Identifie les conditions de concurrence, les sections critiques et les opérations atomiques
      Définis le vocabulaire essentiel de la concurrence, reconnais dans le code les conditions de concurrence liées aux séquences de lecture-modification-écriture et de vérification puis action, et énonce les exigences auxquelles doit satisfaire une solution correcte au problème des sections critiques.
  3. Utilise des mutex POSIX pour rendre les sections critiques atomiques, détermine ce que chaque verrou protège et quelle portion de code il couvre, et évite les erreurs qui compromettent silencieusement le verrouillage.
    • Verrouille et déverrouille un mutex POSIX autour des sections critiques
      Explique ce que garantit un verrou, initialise et utilise correctement un mutex pthread, et corrige avec lui les conditions de concurrence sur un compteur partagé et un compte bancaire.
    • Choisis la granularité du verrouillage et évalue les verrous
      Détermine la quantité de données que protège chaque verrou et la portion de code qu’il couvre, puis évalue un verrou selon sa justesse, son équité et ses performances.
    • Évite les erreurs courantes de verrouillage
      Reconnais les erreurs typiques qui laissent des données sans protection ou des fils en attente indéfinie, et corrige chacune d’elles.
  4. Montre pourquoi les chargements et stockages ordinaires ne suffisent pas à construire un verrou correct sur le matériel moderne, construis des verrous à attente active à partir d’instructions atomiques, rends-les équitables et explique comment les mécanismes du système d’exploitation évitent l’attente active inutile.
    • Explique pourquoi les tentatives simples de mise en œuvre d’un verrou échouent
      Analyse trois premières approches de l’exclusion mutuelle, la désactivation des interruptions, un simple indicateur et l’algorithme de Peterson, et explique où chacune échoue.
    • Construis un verrou à attente active avec des instructions atomiques
      Décris le test et positionnement, la comparaison et échange, le chargement lié et écriture conditionnelle et la lecture et addition, construis un verrou à attente active à partir de ces opérations et évalue-le.
    • Rends le verrouillage à attente active équitable grâce à un verrou à tickets
      Construis un verrou à tickets à partir de la lecture et addition, suis son fonctionnement et explique comment il garantit que chaque fil en attente obtient son tour.
    • Évite l’attente active inutile grâce à la cession du processeur, à la mise en sommeil et aux verrous à deux phases
      Explique comment la cession du processeur, la mise en sommeil dans une file, les futex et les verrous à deux phases réduisent le gaspillage de temps processeur, détermine quand l’attente active vaut la peine et reconnais une inversion de priorité.
  5. Rends les compteurs, les listes chaînées, les files et les tables de hachage sûrs pour de nombreux fils grâce aux verrous, puis évalue chaque conception selon sa justesse et sa capacité à passer à l’échelle lorsque des fils sont ajoutés.
    • Construis des compteurs qui passent à l’échelle
      Protège un compteur avec un seul verrou, explique pourquoi il passe mal à l’échelle et construis un compteur approché avec des compteurs par cœur et un seuil.
    • Rends les listes chaînées sûres pour les accès de plusieurs fils d’exécution
      Protège une liste chaînée avec un seul verrou en gardant les sections critiques courtes et en déverrouillant sur chaque chemin, puis évalue quand un verrouillage par nœud est utile.
    • Conçois des files et des tables de hachage concurrentes
      Construis une file à deux verrous avec un nœud sentinelle et une table de hachage avec un verrou par alvéole, puis adopte une approche fondée sur des mesures pour affiner la granularité du verrouillage.

Fils d’exécution : cours

5 chapitres

1

Les fils d’exécution et leur mémoire partagée

Un processus donne à un programme un espace d’adressage ; les fils d’exécution lui donnent plusieurs chemins d’exécution dans cet espace. Commence par distinguer l’état propre à chaque fil des objets auxquels ils peuvent tous accéder. Utilise ensuite l’interface des fils d’exécution POSIX pour démarrer des travailleurs et recueillir leurs résultats. Les principales questions de conception concernent la possession de chaque argument, la durée de vie de sa mémoire et le moment où le fil créateur peut lire le résultat en toute sécurité.

1.1 Un espace d’adressage, plusieurs chemins d’exécution

Un fil d’exécution est une séquence d’exécution au sein d’un processus. Plusieurs fils peuvent travailler sur les mêmes données du programme, mais chacun doit avoir sa propre position dans le code et ses propres appels de fonction inachevés.

Ce qui est partagé et ce qui appartient à un fil

Les fils d’un processus partagent le code, les variables globales et statiques, les objets du tas, les descripteurs de fichier ouverts et l’identifiant du processus. Chaque fil possède son propre compteur ordinal, ses registres, sa pile et son identifiant. Une variable globale int total désigne un seul objet partagé. Une variable locale ordinaire int i dans la fonction du travailleur désigne un objet différent à chaque appel.

Un processus à trois fils possède trois piles à des adresses différentes dans le même espace d’adressage. Une pile n’est pas un compartiment protégé : un autre fil peut accéder à une variable locale s’il reçoit son adresse. Ce partage n’est valide que pendant l’existence de la variable. Un pointeur vers le cadre de pile d’une fonction terminée ne peut pas maintenir la variable en vie. De grands tableaux locaux et une récursion profonde peuvent aussi épuiser la pile d’un fil.

Passage d’un fil à l’autre

Le système d’exploitation sauvegarde l’état d’exécution de chaque fil dans un bloc de contrôle de fil d’exécution, ou TCB. Lorsqu’il passe de T1 à T2, il sauvegarde les registres de T1 et restaure ceux de T2. Le calcul inachevé de T1 reste dans ses registres sauvegardés jusqu’à sa reprise.

Pour les fils d’un même processus, l’espace d’adressage reste le même pendant la commutation. Les tables des pages n’ont pas besoin de changer, ce qui rend généralement cette commutation moins coûteuse qu’une commutation entre processus. La mémoire partagée implique aussi des défaillances partagées : une écriture par un pointeur invalide peut endommager les données d’un autre fil, et une erreur qui termine le processus met fin à tous ses fils.

Pourquoi utiliser plusieurs fils ?

Le parallélisme désigne une exécution au même instant sur des cœurs de processeur différents. Quatre parties indépendantes d’un calcul peuvent s’exécuter près de quatre fois plus vite sur quatre cœurs, si le travail partagé et la coordination ne deviennent pas des goulots d’étranglement. Les fils permettent aussi à un programme de travailler pendant une attente : pendant que l’un attend une lecture disque ou une réponse réseau, un autre peut utiliser le processeur.

La concurrence signifie que des exécutions progressent pendant des périodes qui se chevauchent. Sur un cœur, l’ordonnanceur passe d’un fil à l’autre : ils sont donc concurrents sans s’exécuter en parallèle. Un seul cœur permet tout de même des bogues de concurrence : une commutation peut interrompre une mise à jour partagée entre ses instructions machine.

Qui ordonnance les fils ?

Linux et Windows utilisent un modèle un à un : chaque fil applicatif ordinaire correspond à un fil ordonnancé par le noyau. Différents fils peuvent s’exécuter sur différents cœurs et un appel bloquant bloque le fil qui l’a effectué. Une bibliothèque plusieurs à un ordonnance plusieurs fils utilisateur sur un seul fil noyau. Ses commutations peuvent coûter peu, mais elle ne peut pas exécuter ces fils sur plusieurs cœurs et un appel système bloquant peut arrêter tout le groupe.

Créer A avant B ne garantit pas que A s’exécute en premier. Si A affiche « A » et B affiche « B », le résultat peut être « AB » ou « BA ». L’ordre de création détermine quand un fil devient disponible pour s’exécuter ; l’ordonnanceur choisit le moment de son exécution.

Points clés

  • Les fils partagent l’espace d’adressage du processus, mais chacun possède ses propres registres, compteur ordinal et pile.
  • Un pointeur peut rendre une variable de pile partagée ; sa durée de vie prend toujours fin avec la fonction à laquelle elle appartient.
  • La concurrence est possible sur un seul cœur ; l’exécution parallèle nécessite plusieurs cœurs.
  • L’ordonnanceur choisit le prochain fil prêt à s’exécuter.
  • Le modèle de fils d’exécution un à un permet l’exécution parallèle et le blocage individuel des fils.

1.2 Démarre des travailleurs et recueille leurs résultats

L’interface des fils d’exécution POSIX, ou pthreads, démarre une fonction C dans un autre fil. Inclus <pthread.h> et compile avec une commande telle que gcc -pthread -o sum sum.c. L’option -pthread fournit les paramètres de compilation et d’édition de liens pour le code utilisant des fils.

Créer un fil avec des arguments stables

L’appel pthread_create(&t, NULL, worker, arg) stocke un identifiant de fil dans t et démarre worker(arg). Le pointeur d’attributs NULL demande les valeurs par défaut. Un travailleur a la signature void *worker(void *arg) : l’argument et la valeur de retour sont des pointeurs. Une structure permet à ce pointeur unique de transmettre plusieurs champs.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

struct range { int lo, hi; long result; };

void *sum_range(void *arg) {
    struct range *r = arg;
    long sum = 0;
    for (int n = r->lo; n <= r->hi; n++)
        sum += n;
    r->result = sum;
    return NULL;
}

Le créateur prépare deux structures, {1, 50, 0} et {51, 100, 0}, et transmet une adresse différente à chaque travailleur. Garde les deux structures en vie jusqu’à la fin des travailleurs. Chaque travailleur accumule sa somme dans sa propre variable locale sum et n’écrit que dans son propre champ result.

pthread_create renvoie 0 en cas de succès et un numéro d’erreur en cas d’échec. Vérifie directement ce numéro ; la fonction ne définit pas errno pour signaler l’échec. Seule une création réussie fournit un fil dont on peut effectuer la jonction.

Effectuer la jonction avant de lire les résultats

Après avoir créé les deux travailleurs avec succès, le fil principal appelle pthread_join(t[0], NULL) et pthread_join(t[1], NULL). Une jonction bloque jusqu’à la fin du fil visé. Une jonction réussie rend également les écritures de ce travailleur visibles pour le fil qui effectue la jonction. Le fil principal peut alors lire 1 275 et 3 775 et les additionner pour obtenir 5 050 sans mutex : les travailleurs ont écrit dans des objets distincts et le fil principal a attendu avant de les lire.

Démarrer les deux fils avant d’effectuer leur jonction permet à leurs travaux de se chevaucher. Créer un travailleur et effectuer immédiatement sa jonction avant de créer le suivant les fait s’exécuter l’un après l’autre.

Voici le programme principal. Il s’arrête en cas d’erreur pthread et n’utilise donc jamais un identifiant issu d’une création qui a échoué :

static void require_success(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

int main(void) {
    struct range jobs[2] = {{1, 50, 0}, {51, 100, 0}};
    pthread_t threads[2];
    for (int i = 0; i < 2; i++)
        require_success(pthread_create(&threads[i], NULL,
                                      sum_range, &jobs[i]));
    for (int i = 0; i < 2; i++)
        require_success(pthread_join(threads[i], NULL));
    printf("%ld\n", jobs[0].result + jobs[1].result);
    return 0;
}

Pour recueillir un pointeur renvoyé, transmets l’adresse où le recevoir : void *answer; pthread_join(t, &answer);. Le pointeur doit désigner une zone mémoire qui survit au travailleur, comme de la mémoire allouée sur le tas ou une structure appartenant au créateur. Renvoyer &local_result depuis le travailleur est invalide, car sa variable locale cesse d’exister au retour de la fonction.

Éviter de partager une variable de boucle qui change

Transmettre &i à chaque travailleur dans une boucle de création revient à transmettre plusieurs fois la même adresse. Les travailleurs peuvent la lire après que la boucle l’a modifiée : plusieurs peuvent donc recevoir le même identifiant apparent. Utilise plutôt un tableau : affecte ids[i] = i, transmets &ids[i] et garde le tableau en vie et inchangé jusqu’à la fin de toutes les jonctions.

Terminer les fils et les processus délibérément

Le retour de main, ou l’appel de exit() dans n’importe quel fil, termine tout le processus et peut interrompre les travailleurs. Le fil principal effectue normalement leur jonction avant de se terminer. L’appel de pthread_exit() ne termine que le fil appelant ; les autres peuvent continuer.

Un fil détaché libère automatiquement ses ressources de fil lorsqu’il se termine et ne peut pas faire l’objet d’une jonction. Veille à ce que chaque fil joignable fasse une fois l’objet d’une jonction ou soit détaché. La jonction attend la fin du fil ; elle ne protège pas deux travailleurs qui mettent à jour le même objet pendant qu’ils s’exécutent tous deux.

Points clés

  • Vérifie directement les codes de retour pthread ; effectue uniquement la jonction des fils créés avec succès.
  • Transmets des objets d’argument distincts et stables et garde-les en vie jusqu’à la fin des travailleurs.
  • Effectue la jonction avant de lire le résultat d’un travailleur qui peut encore être en train de l’écrire.
  • Ne renvoie des pointeurs que vers des objets qui survivent au travailleur.
  • Détache un fil lorsque ses ressources doivent être récupérées sans jonction.
  • Le retour de main termine le processus, y compris les travailleurs qui n’ont pas fini.
2

Repère les étapes partagées qui peuvent interférer

Une instruction source peut cacher plusieurs opérations machine. Suivre ces opérations dans l’ordre rend visible une défaillance de concurrence : un fil lit, un autre modifie l’objet, puis le premier stocke un résultat fondé sur sa lecture périmée. Utilise des suivis pour repérer la séquence partagée qui doit rester indivisible. Distingue ensuite une course aux données d’une erreur plus générale liée à l’ordre des opérations, et évalue une solution selon l’exclusion, la progression et l’attente.

2.1 Suis une mise à jour perdue instruction par instruction

L’instruction counter++ semble simple, mais l’incrément d’un entier partagé ordinaire n’est pas une opération indivisible. Il charge généralement une valeur, effectue une addition dans un registre et stocke le résultat. Un autre fil peut s’exécuter entre ces étapes.

Un suivi qui perd un incrément

Pars de counter égal à 50. Considère le chargement, l’addition et le stockage comme des étapes machine distinctes :

ÉtapeFil en cours d’exécutionActionCompteur partagé
1T1Charge 50 et ajoute 1 dans son registre50
2T2Charge 50 et ajoute 1 dans son registre50
3T2Stocke la valeur de son registre, 5151
4T1Stocke la valeur de son registre, 5151

Deux incréments auraient dû produire 52. T1 écrase au contraire le résultat de T2 avec une valeur calculée à partir de l’ancienne valeur 50. C’est une mise à jour perdue.

Une commutation de contexte conserve les registres de T1 dans son TCB. Lorsque T1 reprend, son registre contient toujours 51 ; il ne recharge pas le compteur partagé simplement parce que T2 s’est exécuté. L’erreur réside dans la séquence non protégée, pas dans la restauration d’un registre incorrect par l’ordonnanceur.

Comparer avec une exécution en série

Si T1 termine les trois étapes avant que T2 ne charge la valeur, T1 stocke 51 puis T2 stocke 52. Inverser l’ordre d’exécution des fils entiers donne aussi 52. Le résultat entrelacé 51 ne peut provenir d’aucun des deux ordres en série. Pour repérer ce type de défaillance, marque une lecture, place une écriture d’un autre fil après celle-ci, puis reprends le calcul du premier fil fondé sur sa lecture périmée.

Ce suivi est un modèle du bogue au niveau machine. En C réel, des accès conflictuels non synchronisés à un entier partagé ordinaire constituent une course aux données, qui entraîne un comportement indéfini. Le C ne limite pas ce programme aux résultats d’un simple modèle d’entrelacement d’instructions.

Pourquoi la réussite des tests est une preuve faible

Deux fils effectuant chacun 10 000 000 d’incréments devraient produire 20 000 000. Des exécutions expérimentales non protégées peuvent afficher différents totaux plus faibles, comme 19 345 221 et 19 221 041 dans des exemples publiés. Une exécution dont le résultat paraît correct signifie seulement que le résultat observé était acceptable cette fois-là.

La fenêtre de vulnérabilité peut ne durer que quelques instructions. Un nombre de cœurs différent, d’autres choix du compilateur ou la charge du système peuvent révéler un ordonnancement que des milliers d’exécutions précédentes n’ont pas rencontré. Pour deux fils effectuant chacun un incrément modélisé à partir de zéro, le modèle de suivi permet d’obtenir 1 ou 2. Avec au moins deux incréments par fil, de rares ordonnancements peuvent laisser un total aussi faible que 2 même lorsque le nombre d’incréments prévus augmente.

Corriger l’opération, pas son apparence

Déclarer l’entier volatile ne rend pas son chargement, son addition et son stockage indivisibles et ne synchronise pas les fils. Écrire tout le travail sur une seule ligne ne change rien. Même une instruction x86 qui ajoute directement à la mémoire doit utiliser la forme atomique appropriée pour assurer la coordination entre les cœurs.

Protège toute la lecture-modification-écriture avec un mutex partagé ou utilise une opération atomique de lecture-modification-écriture de C11 telle que atomic_fetch_add. Ce besoin existe aussi sur un seul cœur : une commutation déclenchée par l’horloge peut séparer le chargement du stockage, même sans exécution parallèle.

Points clés

  • Un incrément ordinaire peut comprendre un chargement, une addition et un stockage.
  • Un stockage fondé sur une lecture périmée peut écraser une mise à jour achevée par un autre fil.
  • Les commutations de contexte conservent les registres privés de chaque fil.
  • Volatile ne fournit ni atomicité ni synchronisation des fils.
  • Une course aux données en C entraîne un comportement indéfini ; un suivi illustre la défaillance sans en délimiter les effets.

2.2 Garde ensemble une décision et sa mise à jour

Une erreur de concurrence peut concerner un accès mémoire ou toute une règle métier. Identifie à la fois les objets partagés et l’opération qui doit paraître indivisible aux autres fils.

Condition de concurrence ou course aux données ?

Une condition de concurrence se produit lorsque la justesse dépend du moment relatif des opérations. Une course aux données se produit lorsque différents fils accèdent au même emplacement mémoire sans synchronisation, qu’au moins l’un y écrit et qu’au moins un accès est non atomique. En C11 et C++11, une course aux données entraîne un comportement indéfini.

Des accès atomiques peuvent éviter une course aux données alors qu’une opération plus large présente toujours une condition de concurrence. Par exemple, des lectures et écritures protégées individuellement ne maintiennent pas la validité d’une décision entre ces accès. Deux fils peuvent chacun lire un solde suffisant, libérer le verrou, puis effectuer un retrait sur la base de cette décision périmée.

Étudier un retrait

Un solde commence à 100. Deux fils demandent chacun 80. La règle consiste à n’accepter un retrait que si le solde le permet.

  1. T1 vérifie 100 et décide que son retrait peut réussir.
  2. T2 vérifie aussi 100 et prend la même décision.
  3. T1 soustrait 80, ce qui laisse 20.
  4. T2 agit sur la base de sa décision antérieure et soustrait 80 de 20, ce qui laisse −60.

Protéger uniquement la soustraction ne corrige pas cette séquence. Protège la vérification et la soustraction avec le même verrou, détenu sans interruption. Un fil laisse alors 20, et le suivant vérifie 20 et refuse le retrait. Un autre ordonnancement non protégé peut perdre une soustraction et laisser 20 après le versement des deux retraits, ce qui est également incorrect.

Repérer la section critique

Une section critique est la portion de code accédant aux données partagées qui doit exclure les accès concurrents. Une séquence de lecture-modification-écriture lit d’abord les données, calcule une nouvelle valeur et la réécrit. Une séquence de vérification puis action détermine si une action est autorisée, puis l’effectue. Les opérations sur les listes, l’initialisation différée et la création de fichiers peuvent aussi contenir ces séquences.

Un verrou assure l’exclusion mutuelle : au plus un fil participant à la fois se trouve dans la zone protégée. Une opération atomique apparaît aux autres fils participants comme une seule opération complète ; ils ne peuvent pas observer un état inachevé. Un mutex n’offre cette vue qu’au code qui respecte la même règle de verrouillage.

Tous les objets n’ont pas besoin d’un verrou. Les données auxquelles un seul fil accède restent privées tant que leur adresse n’est pas partagée. Les données entièrement initialisées avant le démarrage des travailleurs et seulement lues ensuite peuvent être partagées sans verrouillage. Détermine qui peut accéder à un objet et si des écritures se chevauchent avec son utilisation, plutôt que de te limiter à l’endroit où il a été déclaré.

Ce que garantit une solution correcte
  • Exclusion mutuelle : les fils concurrents ne peuvent pas entrer ensemble.
  • Progression : lorsque la section est libre et que des fils souhaitent y entrer, les fils qui n’essaient pas d’entrer ne peuvent pas retarder indéfiniment le choix d’un entrant.
  • Attente bornée : après une demande, le nombre d’entrées concurrentes qui peuvent la précéder est limité.

En C11, atomic_fetch_add(&count, 1) effectue une mise à jour atomique sur un atomic_int. L’atomicité est garantie même sur une plateforme qui implémente l’opération avec un verrou interne de bibliothèque. Rendre atomiques individuellement deux champs de comptes ne transforme toujours pas un transfert entre eux en une transaction atomique unique ; la relation entre les champs doit aussi être protégée.

Points clés

  • Les courses aux données concernent des accès mémoire conflictuels non synchronisés ; les conditions de concurrence concernent une justesse qui dépend du moment des opérations.
  • Une vérification et l’action qui en dépend doivent se trouver dans une même section critique.
  • L’exclusion mutuelle, la progression et l’attente bornée sont des exigences distinctes.
  • Des accès atomiques distincts ne rendent pas atomique une opération en plusieurs étapes.
3

Protège les opérations partagées avec des mutex

Un verrou fonctionne grâce à une règle partagée : chaque opération sur les données protégées suit la même règle. Découvre le cycle de vie d’un mutex POSIX, puis détermine où une section critique commence et se termine. Des sections courtes réduisent l’attente, mais une vérification doit rester avec l’action qu’elle autorise. Chercher dans le code les déverrouillages manquants, les verrous incohérents et les durées de vie non sûres permet de vérifier et de maintenir cette règle.

3.1 Utilise un mutex de son initialisation à sa libération

Un mutex est un verrou qui permet à un seul fil à la fois de posséder une zone protégée. Un fil l’acquiert avant d’accéder aux données protégées et le libère ensuite. Si un autre fil le détient, un appel de verrouillage bloquant attend.

Initialiser le mutex partagé

Utilise pthread_mutex_t de <pthread.h>. Un mutex statique peut être initialisé avec pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;. Un objet créé dynamiquement peut initialiser son champ avec pthread_mutex_init(&obj->lock, NULL). L’initialisation doit être terminée avant qu’un fil utilise le mutex.

Voici une opération sur un compteur utilisant un mutex partagé initialisé statiquement. La fonction auxiliaire vérifie les valeurs renvoyées par pthread, qui sont directement des numéros d’erreur :

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static long counter = 0;  // guarded by m

static void check(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

void increment(void) {
    check(pthread_mutex_lock(&m));
    counter = counter + 1;
    check(pthread_mutex_unlock(&m));
}

Toute lecture ou mise à jour concurrente de counter doit suivre cette même règle. Une fonction distincte utilisant un autre mutex ne serait pas coordonnée avec increment.

Que se passe-t-il pendant que le mutex est détenu ?

T1 verrouille m, charge le compteur, calcule et stocke le résultat. Si T2 appelle pthread_mutex_lock(&m) pendant cet intervalle, il ne peut pas poursuivre après l’appel avant d’avoir acquis le mutex une fois que T1 l’a libéré. T1 peut toujours subir une préemption : un mutex empêche les entrées concurrentes, pas les commutations de contexte.

Pour la règle du solde, détiens le mutex avant d’évaluer balance >= amount. Ne soustrais la somme que si la vérification réussit, enregistre le succès ou l’échec, puis déverrouille. Un solde de 100 avec deux demandes de 80 permet exactement un retrait et laisse 20.

Choisir comment attendre

pthread_mutex_trylock(&m) revient immédiatement : 0 signifie que l’appelant a acquis le mutex ; EBUSY signifie qu’il ne l’a pas acquis parce que le mutex était détenu. Dans ce dernier cas, il peut effectuer un autre travail et ne doit pas accéder aux données protégées. pthread_mutex_timedlock attend jusqu’à une échéance absolue. Ces interfaces exigent toujours de vérifier les codes de retour et de ne libérer un verrou qu’après une acquisition réussie.

Respecter la possession et la durée de vie

Le fil possesseur libère le mutex. Un mutex normal non récursif bloque si son possesseur essaie de le verrouiller à nouveau. Un mutex avec vérification d’erreurs signale les erreurs comme un nouveau verrouillage avec EDEADLK ou un déverrouillage par un autre fil avec EPERM. Un mutex récursif compte les acquisitions répétées par son possesseur et exige le même nombre de libérations. Ne compte pas sur un mutex par défaut pour diagnostiquer une utilisation incorrecte : son comportement dépend du type auquel l’implémentation l’associe et de l’opération.

Lorsque tous les fils ont cessé d’utiliser le mutex et qu’il est déverrouillé, appelle pthread_mutex_destroy. Détruire ou libérer un objet alors qu’un autre fil peut encore le verrouiller compromet la durée de vie du mutex. Transmettre le mutex par pointeur préserve son identité ; le copier ne crée pas un autre accès valide au même verrou.

Points clés

  • Initialise un mutex avant de le rendre accessible aux travailleurs.
  • Acquiers le même mutex partagé avant chaque accès concurrent aux données qu’il protège.
  • Un mutex détenu empêche les entrées concurrentes, même si son possesseur peut toujours subir une préemption.
  • Lorsque trylock renvoie EBUSY, le mutex n’a pas été acquis.
  • Garde le mutex en vie jusqu’à ce qu’aucun fil ne puisse plus l’utiliser.
  • Les mutex avec vérification d’erreurs signalent les erreurs de possession ; les mutex récursifs comptent les acquisitions imbriquées.

3.2 Choisis ce qu’un verrou protège

La granularité du verrouillage est la quantité d’état partagé couverte par un verrou. Elle détermine quelles opérations doivent s’attendre mutuellement, même lorsqu’elles s’exécutent sur des cœurs différents.

Un grand verrou ou plusieurs petits verrous ?

Une conception à gros grain place un mutex autour d’une structure ou d’un sous-système entier. Sa règle de protection est simple, mais des opérations sans lien entre elles sont sérialisées. Une conception à grain fin attribue des mutex distincts à des comptes, des alvéoles ou des zones distincts. Les fils peuvent alors travailler simultanément dans différentes zones, au prix d’un plus grand nombre d’opérations de verrouillage et de règles plus compliquées.

Place le verrou à côté de l’état qu’il protège :

struct account {
    pthread_mutex_t lock;
    long balance;
    unsigned id;
};

Une opération sur un compte prend son mutex. Un transfert entre deux comptes distincts nécessite les deux mutex, car il modifie leur relation. Choisis un ordre fixe, par exemple par identifiant de compte croissant, et utilise-le sur chaque chemin. Sinon, un fil peut détenir A et attendre B pendant qu’un autre détient B et attend A.

Un verrou plafonne le débit

Supposons que chaque opération détienne un verrou global unique pendant 2 µs. Le travail protégé peut achever au plus une opération toutes les 2 µs :

deˊbit maximal=12×10−6=500 000 opeˊrations par seconde.

Ajouter des cœurs ne permet pas à deux opérations de détenir simultanément le même mutex. Le surcoût du verrou et les autres tâches peuvent réduire encore le débit obtenu. La durée de la partie protégée constitue donc une limite utile à calculer avant d’espérer une accélération.

Raccourcir la section sans compromettre l’opération

Prépare le travail privé avant d’acquérir le mutex. Pour une insertion dans une liste, alloue le nouveau nœud et renseigne d’abord sa clé. N’acquiers le mutex de la liste qu’au moment de lire la tête actuelle et de chaîner le nœud. Un échec d’allocation se produit alors sans qu’un verrou soit détenu.

Des entrées-sorties lentes, un affichage ou un rappel inconnu peuvent bloquer tous les fils en attente. Déplace ce travail hors de la section lorsque les données peuvent y rester valides. Garde ensemble une décision et la mise à jour qui en dépend : raccourcir un retrait en déverrouillant entre la vérification du solde et la soustraction compromet sa justesse.

Évaluer trois propriétés distinctes

L’exclusion mutuelle demande si la conception empêche réellement les entrées conflictuelles. L’équité demande si un fil en attente finit par obtenir son tour ou peut subir une famine. Les performances concernent le coût du verrou avec un fil, plusieurs fils sur un processeur et des fils sur plusieurs processeurs. Un verrou peut être correct tout en étant inéquitable ou lent.

La contention se produit lorsque des fils se disputent un verrou déjà détenu. En cas de forte contention, ils déplacent aussi entre les cœurs les lignes de cache du verrou et des données partagées. Dans une expérience publiée sur un compteur, un million de mises à jour par un fil ont pris environ 0,03 s, mais deux fils en effectuant chacun un million ont mis plus de 5 s ; ces mesures concernent cette configuration et ne sont pas des durées universelles.

Pour une expérience de passage à l’échelle, garde fixe le travail de chaque fil. Un passage à l’échelle idéal permet à N fils sur N cœurs d’effectuer N fois plus de travail dans le temps initial. Commence par une structure simple et correcte, mesure l’attente et le temps total, puis répartis un verrou uniquement là où la concurrence supplémentaire compense le surcoût ajouté.

Points clés

  • Les verrous à gros grain simplifient la protection ; les verrous à grain plus fin permettent à des zones indépendantes de progresser simultanément.
  • Un verrou unique sérialise le travail qu’il protège et limite le débit.
  • Prends plusieurs verrous dans un ordre fixe.
  • Évalue séparément l’exclusion, l’équité et les performances.
  • Mesure avant de remplacer un verrou simple par une conception plus compliquée.

3.3 Examine chaque chemin utilisant le verrou

Un mutex peut être parfaitement implémenté alors qu’un programme l’utilise incorrectement. Examine tout le chemin parcouru dans une fonction, y compris les lectures, les erreurs, les rappels et la destruction des objets.

Libérer le verrou à chaque sortie

Cette fonction laisse le verrou détenu sur son chemin d’échec :

pthread_mutex_lock(&m);
if (failed) return -1;
update_shared_state();
pthread_mutex_unlock(&m);

Le fil qui effectue le retour peut se terminer, mais le mutex reste détenu. Un appelant ultérieur attend alors une libération qui ne se produira jamais. Utilise un seul chemin de nettoyage ou place un déverrouillage avant chaque retour. Avec des appels pthread vérifiés, la structure peut être :

int rc = -1;
check(pthread_mutex_lock(&m));
if (!failed) {
    update_shared_state();
    rc = 0;
}
check(pthread_mutex_unlock(&m));
return rc;

Il est plus facile d’examiner un seul point de libération que plusieurs points dispersés. La propriété essentielle est que chaque verrou acquis avec succès soit libéré à chaque sortie ordinaire.

Respecter aussi la règle de protection pour les lectures

Une fonction d’état qui « ne fait que lire » peut être en concurrence avec une fonction de mise à jour. Elle peut aussi observer des champs liés à différentes étapes d’une même mise à jour. Si une opération sur une liste publie un nœud avant d’avoir renseigné tous ses champs, un lecteur non protégé peut suivre un pointeur vers des données inachevées. Acquiers le verrou indiqué pour chaque accès dont la justesse en dépend.

Utiliser des mutex différents pour le même objet ne résout rien. Si T1 prend lockA et T2 prend lockB, les deux acquisitions peuvent réussir. Un mutex déclaré comme nouvelle variable locale dans chaque travailleur rend cette erreur facile à manquer : chaque fil verrouille son propre objet, donc aucune exclusion partagée n’existe.

Préserver l’identité et les limites de l’opération

Un pthread_mutex_t ne peut pas être copié par affectation ou par copie de la structure qui le contient, puis utilisé comme second accès au mutex. Utiliser la copie entraîne un comportement indéfini. Transmets un pointeur vers le mutex partagé d’origine.

Vérifie aussi où les libérations ont lieu. Une fonction qui verrouille pour vérifier un solde, déverrouille, puis verrouille à nouveau pour soustraire une somme a protégé les accès individuels, mais laissé la décision devenir périmée. Garde toute la séquence de vérification puis action sous la protection continue du verrou.

Éviter de s’attendre soi-même

Appeler du code inconnu en détenant un mutex peut entraîner un nouvel accès inattendu au même objet protégé. Avec un mutex normal, un second appel de verrouillage par le possesseur bloque ce dernier. Un mutex avec vérification d’erreurs signale cette erreur, tandis qu’un mutex récursif autorise des acquisitions imbriquées et exige autant de déverrouillages. Choisis le type délibérément ; n’utilise pas la récursivité pour masquer une conception de protection peu claire.

Évite si possible les entrées-sorties lentes et les mises en sommeil dans une section critique partagée. Avant de détruire le mutex ou de libérer l’objet qui le contient, assure-toi qu’aucun fil ne le possède ni ne peut y accéder à nouveau. L’initialisation, l’utilisation et la destruction doivent suivre un ordre clair.

Utiliser des outils d’exécution pour révéler les défaillances observées

ThreadSanitizer, activé avec -fsanitize=thread, et Helgrind de Valgrind peuvent signaler les courses aux données et les utilisations incorrectes de verrous dans les exécutions observées. Combine leurs rapports avec un examen des accès partagés et des entrelacements possibles. Une exécution sans signalement établit que l’outil n’a trouvé aucun problème dans ces exécutions ; un chemin non testé exige toujours un raisonnement.

Points clés

  • Libère chaque mutex acquis avec succès sur chaque chemin de sortie.
  • Les lecteurs doivent respecter la règle de protection lorsque des écritures peuvent avoir lieu simultanément.
  • Un mutex est partagé par son adresse ; des mutex distincts ou copiés ne coordonnent pas les accès.
  • Ne sépare pas une vérification de son action par un déverrouillage.
  • Des rappels inconnus peuvent accéder à nouveau au code et tenter d’acquérir le mutex détenu.
  • Une exécution sans signalement par un détecteur de courses ne couvre que les exécutions observées ; examine aussi les chemins non testés.
4

Comment les verrous sont acquis, attendus et transmis

Un verrou doit rendre indivisible la décision de possession et ordonner les accès mémoire protégés. Les indicateurs ordinaires ne font ni l’un ni l’autre. Pars des tentatives de verrouillage qui échouent pour arriver à l’échange atomique, puis ajoute l’équité grâce aux numéros de tickets. Considère enfin le fil en attente : l’attente active, la cession du processeur et la mise en sommeil ont des coûts différents, et la mise en sommeil nécessite un protocole qui ne peut pas perdre de réveil. La priorité d’ordonnancement peut modifier la stratégie d’attente qui permet de progresser.

4.1 Pourquoi vérifier un simple indicateur ne suffit pas

Un verrou a besoin d’un moyen fiable de choisir un seul possesseur. Une lecture ordinaire suivie d’une écriture ordinaire laisse un intervalle dans cette décision, même si le travail protégé est très court.

Le contrôle des interruptions a un rôle limité

Sur un processeur, un noyau peut désactiver brièvement les interruptions autour d’une section critique non bloquante afin d’y empêcher une préemption provoquée par une interruption. Cela suppose de rester sur le processeur sans se bloquer ni provoquer volontairement une commutation. Le noyau utilise cette technique pour de courtes opérations internes.

Ce n’est pas un mutex général pour les applications. Les instructions de contrôle des interruptions sont privilégiées ; donner à une application un accès illimité lui permettrait de monopoliser le processeur. Désactiver les interruptions sur un cœur n’empêche pas les autres cœurs d’accéder aux mêmes données. De longues périodes de désactivation retardent aussi le traitement des interruptions et peuvent entraîner la perte d’événements sur certains périphériques.

Suivre le verrou à indicateur défaillant

Considère ce pseudocode proche du C :

while (flag == 1) { }
flag = 1;
// critical section
flag = 0;

Pars de flag à 0. T1 le teste et quitte la boucle, puis s’interrompt avant l’affectation. T2 teste le même 0, lui affecte 1 et entre. T1 reprend, lui affecte 1 et entre aussi. Les deux ont passé le test avant que l’un d’eux ne prenne possession du verrou.

Rendre la lecture et l’écriture individuellement atomiques supprimerait une course aux données sur l’indicateur, mais laisserait deux opérations distinctes dans ce protocole. La prise de possession nécessite une lecture-modification-écriture atomique ou un algorithme de synchronisation plus complet.

La boucle vide est une attente active : elle vérifie à répétition tout en consommant du temps processeur. Sur un processeur, un fil qui attend activement après avoir provoqué la préemption du possesseur ne peut pas l’aider à libérer le verrou plus tôt. Il doit finir par laisser le possesseur s’exécuter.

La solution de Peterson pour deux fils

L’algorithme de Peterson coordonne deux fils avec un indicateur d’intérêt par fil et un tour partagé. Le fil i effectue :

flag[i] = 1;
turn = 1 - i;
while (flag[1 - i] == 1 && turn == 1 - i) { }
// critical section
flag[i] = 0;

Il annonce son intérêt, donne la priorité à l’autre fil en cas d’égalité et attend seulement tant que l’autre est intéressé et prioritaire. En cohérence séquentielle, les opérations de chaque fil conservent l’ordre du programme et tous les fils observent un ordre commun. Dans ce modèle, l’algorithme de Peterson assure l’exclusion mutuelle, la progression et l’attente bornée pour deux fils.

L’ordre des accès mémoire compte aussi

Les processeurs et compilateurs modernes peuvent rendre visibles les opérations mémoire dans un ordre différent de celui du code source. Un stockage peut rester dans un tampon pendant qu’un chargement ultérieur lit la mémoire. Les deux fils peuvent alors voir l’indicateur d’intérêt de l’autre à 0 et entrer tous les deux. Des entiers C partagés ordinaires introduisent aussi des courses aux données.

Une implémentation valide nécessite des opérations atomiques et un ordre adaptés, par exemple des opérations atomiques C11 à cohérence séquentielle pour les indicateurs et le tour. volatile ne fournit pas cette synchronisation. Les verrous utilisables en pratique combinent une opération indivisible de prise de possession et des garanties sur l’ordre des accès mémoire, afin que les accès protégés restent du bon côté de l’acquisition et de la libération.

Points clés

  • Séparer le test du positionnement permet à deux fils de prendre le même verrou libre.
  • Désactiver les interruptions sur un processeur n’exclut pas un autre cœur.
  • L’algorithme de Peterson pour deux fils nécessite un comportement à cohérence séquentielle.
  • Une prise de possession atomique et un ordre correct des accès mémoire sont tous deux nécessaires à un verrou utilisable en pratique.

4.2 Utilise une instruction atomique pour choisir un seul possesseur

Une lecture-modification-écriture atomique combine la lecture d’un emplacement et sa mise à jour en une seule action indivisible. Elle ferme l’intervalle qui permet à deux fils de passer ensemble un test ordinaire d’indicateur.

Échanger l’indicateur et examiner son ancienne valeur

Le test et positionnement, ou échange atomique, écrit une nouvelle valeur et renvoie l’ancienne. Avec un indicateur initialement à 0, T1 y échange la valeur 1 et reçoit 0, donc il entre. T2 y échange la valeur 1 et reçoit 1, donc il attend. Tant que la libération n’écrit pas 0, aucun autre échange ne peut renvoyer 0.

Voici un verrou à attente active C11 utilisant les opérations atomiques à cohérence séquentielle par défaut :

#include <stdatomic.h>

static atomic_flag held = ATOMIC_FLAG_INIT;

void lock(void) {
    while (atomic_flag_test_and_set(&held)) { }
}

void unlock(void) {
    atomic_flag_clear(&held);
}

L’ordre compte autant que l’indicateur. L’acquisition doit maintenir les accès ultérieurs de la section critique après l’acquisition réussie ; la libération doit maintenir les accès antérieurs de la section critique avant la libération. La remise à zéro de l’indicateur doit aussi être atomique ; mélanger des stockages ordinaires et des accès atomiques ne permet pas de mettre en œuvre ce passage de relais.

Comparer avant de remplacer

La comparaison et échange, ou CAS, ne modifie une valeur que lorsqu’elle est égale à une valeur attendue. Supposons que deux fils lisent un compteur à 7 et souhaitent chacun ajouter 1. T2 réussit à remplacer 7 par 8. La tentative de T1 pour remplacer 7 par 8 échoue alors : T1 réessaie à partir de 8 et le remplace par 9. La nouvelle tentative préserve les deux incréments.

En C11, atomic_compare_exchange_strong renvoie un succès ou un échec et met à jour l’argument attendu en cas d’échec. Certains pseudocodes proches du matériel renvoient à la place l’ancienne valeur stockée. Vérifie quelle convention un suivi utilise. Sur x86, xchg avec un opérande mémoire effectue un échange, tandis que lock cmpxchg fournit une comparaison et échange atomique.

Autres mécanismes matériels de base

Le chargement lié et écriture conditionnelle, ou LL/SC, lit une valeur tout en établissant une réservation, puis tente une écriture. L’écriture conditionnelle échoue si une autre écriture a invalidé la réservation ; elle peut aussi échouer pour d’autres raisons et doit donc figurer dans une boucle de nouvelles tentatives. Les instructions LDXR/STXR d’ARM et lr/sc de RISC-V prennent en charge cette séquence.

La lecture et addition ajoute une quantité de manière atomique et renvoie l’ancienne valeur. C11 la fournit sous la forme atomic_fetch_add ; x86 fournit lock xadd. Le retour de l’ancienne valeur est utile pour distribuer des numéros de tickets uniques.

Le C garantit le comportement atomique des opérations de sa bibliothèque atomique. Il n’exige pas que chaque type atomique soit sans verrou ; certaines opérations utilisent un verrou interne de bibliothèque. atomic_flag bénéficie de la garantie sans verrou, et atomic_is_lock_free vérifie les autres objets.

Correct ne signifie pas équitable ou rapide

Un verrou fondé sur le test et positionnement assure l’exclusion mutuelle, mais pas l’ordre d’arrivée : un fil peut perdre à répétition la course après une libération et subir une famine. Sur un processeur, quatre fils avec des tranches de 10 ms à tour de rôle peuvent gaspiller 30 ms en attente active pour trois fils en attente avant que le possesseur ayant subi une préemption s’exécute à nouveau. Sur plusieurs cœurs, si le possesseur s’exécute et que les sections sont courtes, une brève attente active peut éviter le surcoût de la mise en sommeil et du réveil.

Points clés

  • Le test et positionnement renvoie l’ancienne valeur de l’indicateur tout en écrivant la nouvelle de manière atomique.
  • La comparaison et échange réessaie après une valeur attendue périmée, ce qui préserve les mises à jour concurrentes.
  • Le code utilisant le chargement lié et écriture conditionnelle doit gérer l’échec de l’écriture conditionnelle en réessayant.
  • La lecture et addition renvoie l’ancienne valeur tout en effectuant l’incrément.
  • L’acquisition et la libération d’un verrou doivent aussi ordonner les accès mémoire protégés.
  • Un verrou à attente active fondé sur le test et positionnement assure l’exclusion, mais ne garantit pas l’absence de famine.

4.3 Distribue les tours avec un verrou à tickets

Un verrou fondé sur le test et positionnement mémorise s’il est détenu, mais pas qui attend. Un verrou à tickets ajoute un ordre d’arrivée à l’aide d’un distributeur atomique de tickets et d’un compteur indiquant le numéro servi.

Prendre un numéro, puis attendre son tour

Les deux compteurs commencent à 0. Chaque appelant incrémente ticket de manière atomique et conserve son ancienne valeur dans mine. Il n’entre que lorsque turn est égal à mine. Le possesseur libère le verrou en augmentant turn d’une unité.

#include <stdatomic.h>

static atomic_uint ticket = 0;
static atomic_uint turn = 0;

void lock(void) {
    unsigned mine = atomic_fetch_add(&ticket, 1);
    while (atomic_load(&turn) != mine) { }
}

void unlock(void) {
    atomic_fetch_add(&turn, 1);
}

Ces opérations atomiques C11 par défaut assurent à la fois l’atomicité et un ordre à cohérence séquentielle. Chaque appelant conserve mine dans sa propre variable locale. Le compteur partagé de tickets distribue des numéros uniques parmi les tickets en cours.

Suivre trois arrivées

A prend le ticket 0 et entre car turn vaut 0. B prend le ticket 1 et attend. C prend le ticket 2 et attend. Le prochain ticket inutilisé est maintenant 3, tandis que le tour courant est toujours 0.

Lorsque A libère le verrou, turn devient 1, donc B entre. C attend toujours le tour 2. La libération par B fait passer turn à 2 et C entre. Un fil nouvellement arrivé reçoit le ticket 3 et ne peut pas passer devant C.

Cette distinction compte lors d’un suivi : ticket indique le prochain numéro à distribuer ; turn indique le numéro servi. Prendre un ticket ne modifie que le distributeur. La libération fait avancer le service.

Ce que garantit l’équité

Un appelant attend derrière les fils ayant des tickets antérieurs. Si chaque détenteur finit par libérer le verrou et que les fils devant lui peuvent s’exécuter, les arrivants ultérieurs ne peuvent pas le dépasser. Le nombre de sections critiques concurrentes devant lui est borné par les tickets antérieurs encore en cours. Cela assure une attente bornée et empêche la famine sous ces conditions de progression.

La garantie concerne l’ordre d’entrée, pas le temps écoulé. Un détenteur qui reste bloqué longtemps peut aussi faire attendre longtemps tous les appelants suivants. Un numéro de ticket ne peut pas obliger l’ordonnanceur à exécuter son détenteur.

Les coûts qui subsistent avec l’ordre des tickets

Chaque fil en attente utilise toujours l’attente active. Si le fil dont le tour arrive ensuite n’est pas ordonnancé, les fils suivants prêts à s’exécuter attendent même si aucun fil n’occupe actuellement la section critique. C’est un coût d’ordonnancement dû à l’ordre strict.

Les fils en attente lisent aussi un même emplacement partagé turn. Chaque libération modifie la ligne de cache qu’ils surveillent tous, ce qui produit du trafic entre les cœurs. Les verrous à attente active fondés sur une file réduisent ce trafic en permettant à chaque fil de surveiller un emplacement local distinct ; Linux est passé des verrous à tickets à des conceptions fondées sur une file.

Les compteurs de tickets réels utilisent l’arithmétique non signée. Les tests d’égalité peuvent fonctionner malgré le rebouclage lorsque le nombre de tickets en cours reste inférieur au nombre de valeurs possibles du compteur. Réutiliser un numéro alors qu’un ticket antérieur de même numéro est encore en cours détruirait l’unicité et l’exclusion.

Points clés

  • L’attribution des tickets est atomique ; un appelant n’entre que lorsque son numéro correspond au tour courant.
  • La libération fait avancer turn, tandis que l’acquisition attribue un numéro à partir de ticket.
  • Les détenteurs de tickets antérieurs ne peuvent pas être dépassés par les arrivants ultérieurs.
  • L’ordre équitable laisse subsister l’attente active, les blocages dus à l’ordonnancement et le trafic de cache partagé.

4.4 Laisse les fils en attente dormir sans manquer une libération

Un verrou correct peut gaspiller la majeure partie du temps d’un processeur en attente. La bonne stratégie dépend de la possibilité pour le possesseur de s’exécuter et de la durée pendant laquelle il détiendra le verrou.

Céder le processeur fait passer son tour

Un fil en attente peut appeler yield() après une tentative de prise de possession échouée. La cession le fait passer de l’état en cours d’exécution à l’état prêt afin que l’ordonnanceur puisse choisir un autre fil. Il reste prêt à s’exécuter : il peut donc être rapidement choisi et échouer à nouveau. Avec 100 fils sur un processeur, jusqu’à 99 fils en attente peuvent chacun s’exécuter et céder le processeur avant qu’un possesseur ayant subi une préemption obtienne un nouveau tour. La cession réduit l’attente active, mais ne garantit pas un ordre d’entrée équitable.

Le blocage retire le fil des tâches prêtes à s’exécuter

Un verrou avec mise en sommeil place un appelant qui n’a pas réussi à l’acquérir dans une file d’attente. Celui-ci ne consomme pas de temps processeur pendant qu’il est bloqué. Le déverrouillage réveille un fil en attente afin qu’il puisse s’exécuter à nouveau. Un petit verrou de garde interne à attente active peut protéger l’indicateur et la file ; il n’est détenu que pendant de brèves opérations de gestion.

Séparer « décider de dormir » de « dormir effectivement » crée un risque de réveil perdu :

  1. Un fil en attente se place dans la file et libère le verrou de garde.
  2. Il est interrompu avant d’appeler park().
  3. Le possesseur libère le verrou et appelle unpark() pour ce fil.
  4. Si ce réveil est ignoré, le fil se met ensuite en sommeil alors que personne ne peut plus le réveiller.

L’appel setpark() de Solaris annonce l’intention de dormir. Un appel de réveil intervenant entre-temps fait alors revenir immédiatement l’appel de mise en sommeil ultérieur. Le protocole relie la décision au blocage effectif.

Linux utilise une comparaison et un blocage atomiques

Un futex combine un mot en mémoire utilisateur et une prise en charge de l’attente par le noyau. futex_wait(addr, expected) ne bloque que si le mot est toujours égal à la valeur attendue ; la vérification et la décision de blocage sont atomiques vis-à-vis des opérations sur ce mot futex. Si un déverrouillage l’a déjà modifié, l’appelant ne se met pas en sommeil sur la base de la valeur attendue périmée.

futex_wake(addr, n) réveille jusqu’à n fils en attente. Un verrou sans contention peut être acquis et libéré avec des instructions atomiques en espace utilisateur, sans appel système. Les mutex pthread de glibc sous Linux utilisent les futex pour les attentes avec contention. Un réveil rend un fil prêt à s’exécuter ; son protocole de verrouillage doit encore établir sa possession avant qu’il accède aux données protégées.

Attendre activement un instant, puis dormir

Un verrou à deux phases attend d’abord activement pendant un court moment, puis bloque si l’attente se prolonge. L’attente active peut être utile lorsque le possesseur s’exécute sur un autre cœur et que l’attente prévue est plus courte que la mise en sommeil suivie du réveil. Le blocage est préférable pour les longues attentes ou lorsque les fils prêts à s’exécuter sont plus nombreux que les cœurs. Sur un processeur, un fil en attente active ne peut pas aider un possesseur non ordonnancé à finir.

La priorité d’ordonnancement peut empêcher la progression

Dans une inversion de priorité, un fil de priorité élevée attend un mutex détenu par un fil de faible priorité, tandis que des tâches de priorité moyenne empêchent le possesseur d’accéder au processeur. Mettre en sommeil le fil de priorité élevée évite l’attente active, mais ne supprime pas ce délai d’ordonnancement.

L’héritage de priorité élève temporairement la priorité du possesseur à celle du fil en attente le plus prioritaire, lui permettant de finir et de libérer le verrou. Le Mars Pathfinder de la NASA a subi des redémarrages dus à une inversion de priorité en 1997 ; l’activation de l’héritage de priorité du mutex a corrigé le problème. Un fil de priorité élevée en attente active sur un processeur unique avec un ordonnancement strict par priorité peut avoir un effet pire : il peut empêcher complètement le possesseur de faible priorité de s’exécuter.

Points clés

  • La cession du processeur laisse un fil en attente prêt à s’exécuter ; la mise en sommeil le bloque sans consommer de temps processeur.
  • Un protocole de mise en sommeil correct ferme l’intervalle entre la décision de dormir et le blocage effectif.
  • L’attente sur un futex vérifie sa valeur attendue de manière atomique avec la décision de blocage.
  • Un verrou à deux phases attend activement un court moment avant de se mettre en sommeil.
  • L’héritage de priorité aide un possesseur de faible priorité à libérer un verrou nécessaire à une tâche plus prioritaire.
5

Construis des structures partagées qui restent correctes

Une structure est sûre vis-à-vis des fils d’exécution lorsque les appels concurrents respectent sa spécification pour tous leurs entrelacements possibles. Commence avec un mutex unique et une règle explicite pour chaque champ partagé. Examine ensuite où les opérations doivent réellement s’exclure : les mises à jour de compteurs, les parcours de listes, les extrémités opposées d’une file et les différentes alvéoles de hachage ont des besoins différents. Chaque amélioration doit préserver une publication sûre et la durée de vie de la mémoire, tout en justifiant sa complexité supplémentaire par des mesures.

5.1 Échange de la précision contre moins de mises à jour partagées

Un compteur partagé est facile à protéger avec un seul mutex. Ses opérations increment, decrement et get acquièrent chacune le même verrou, agissent et le libèrent. Il est exact, mais toutes les mises à jour passent par un seul point partagé.

Pourquoi le compteur exact devient un goulot d’étranglement

Sur plusieurs cœurs, un mutex sérialise les opérations sur le compteur. Les cœurs transfèrent aussi la possession des lignes de cache du verrou et du compteur. Ajouter des travailleurs peut augmenter l’attente plus vite que le travail utile. Remplacer le mutex par atomic_fetch_add supprime le surcoût explicite du verrou sur les processeurs courants, mais chaque mise à jour écrit toujours au même emplacement partagé.

Un compteur approché répartit les mises à jour entre des compteurs locaux et les transfère par lots vers un compteur global. La valeur globale qu’il indique omet délibérément les incréments locaux récents.

Construire la mise à jour du local vers le global

Conserve un compteur et un mutex par cœur, un compteur global avec son propre mutex et un seuil S. Pour chaque incrément :

  1. Choisis l’emplacement local pour cette mise à jour et acquiers son mutex.
  2. Ajoute 1 au décompte local.
  3. S’il atteint S, acquiers le mutex global tout en détenant encore le mutex local.
  4. Ajoute ce décompte local au décompte global, remets le décompte local à 0 et libère le mutex global.
  5. Libère le mutex local.

Garde le même emplacement pendant toute la mise à jour. Les fils qui utilisent des emplacements différents touchent généralement des verrous différents et n’acquièrent le mutex global que lors d’un transfert. La lecture du décompte global suit aussi sa règle de verrouillage.

Suivre un transfert par lot

Avec quatre cœurs et S égal à 5, supposons que leurs totaux de mises à jour soient 7, 3, 4 et 0. Le cœur 1 transfère 5 à son cinquième incrément et conserve ensuite 2 incréments locaux. Les décomptes locaux finaux sont 2, 3, 4 et 0. Le décompte global est de 5 et le total réel de 14 : neuf incréments restent locaux.

Après des mises à jour achevées par incréments unitaires, chaque décompte local est compris entre 0 et S − 1. Avec P cœurs, le retard du total global peut atteindre au plus :

P(S−1).

Pour quatre cœurs et S égal à 5, la borne est de 16. Cette borne concerne les mises à jour achevées et les décomptes locaux restants ; ne l’applique pas au milieu d’un transfert.

Choisir le seuil selon le travail

Un seuil plus petit rend la valeur plus actuelle, mais nécessite davantage d’acquisitions du verrou global. Avec S égal à 1, chaque incrément est transféré au niveau global, ce qui ajoute le surcoût du verrou local sans éviter le goulot d’étranglement global. Un seuil plus grand améliore le passage à l’échelle des mises à jour, mais augmente le retard possible. Dans une expérience publiée, S égal à 1024 a permis à quatre fils d’effectuer quatre millions de mises à jour presque dans le temps nécessaire à un fil pour en effectuer un million.

Un instantané exact doit détenir tous les mutex locaux dans un ordre fixe, puis le mutex global avant d’additionner les décomptes. Cet ordre est cohérent avec les mises à jour qui prennent un verrou local avant le verrou global. L’instantané arrête les transferts concurrents pendant la lecture ; lire chaque champ séparément pourrait compter un transfert deux fois ou le manquer.

Les compteurs approchés conviennent aux statistiques qui tolèrent une visibilité différée. Un compteur de références qui détermine quand la mémoire peut être libérée nécessite une méthode exacte : un décompte sous-estimé pourrait libérer un objet encore utilisé.

Points clés

  • Un compteur atomique unique peut encore constituer un goulot d’étranglement dans le cache partagé.
  • Les compteurs approchés regroupent les incréments locaux par lots dans un compteur global protégé.
  • Après des mises à jour unitaires achevées, le retard maximal est P(S − 1).
  • Des seuils de transfert plus élevés réduisent la contention globale tout en augmentant le retard possible.
  • Un instantané exact détient tous les verrous concernés dans un ordre cohérent avec les mises à jour.

5.2 Publie et parcours les nœuds d’une liste chaînée en toute sécurité

Une liste chaînée partagée doit préserver à la fois ses liens et la durée de vie de ses nœuds. Un mutex autour de chaque opération constitue une première conception utile, car chaque parcours et chaque modification partagés suivent une seule règle.

Comment deux insertions perdent un nœud

Supposons que la liste commence à A. T1 prépare le nœud X et T2 prépare le nœud Y. Sans verrou partagé, tous deux peuvent lire head == A, affecter A au champ next de leur nouveau nœud, puis stocker leur propre nœud dans head. La dernière affectation de la tête masque le premier nœud inséré. Ce nœud devient inaccessible : une insertion est perdue et sa mémoire fuit.

La séquence protégée doit comprendre la lecture de la tête actuelle, la définition du nouveau lien et la publication de la nouvelle tête. Un verrou placé uniquement autour de l’affectation finale permettrait aux deux fils de construire leurs liens à partir de la même tête périmée.

Préparer les données privées avant d’acquérir le verrou

Un nouveau nœud reste privé jusqu’à son chaînage dans la liste partagée. L’allocation et l’initialisation de la clé peuvent avoir lieu avant l’acquisition. Avec la fonction auxiliaire qui vérifie les appels pthread, présentée plus tôt, l’insertion peut s’écrire :

int insert(list_t *list, int key) {
    node_t *node = malloc(sizeof(*node));
    if (node == NULL) return -1;
    node->key = key;

    check(pthread_mutex_lock(&list->lock));
    node->next = list->head;
    list->head = node;
    check(pthread_mutex_unlock(&list->lock));
    return 0;
}

Un échec d’allocation provoque un retour avant qu’un mutex soit détenu. La section partagée reste courte, tandis que la vérification de head et la mise à jour du lien restent ensemble. Une allocation sûre vis-à-vis des fils ne rend pas la liste elle-même sûre : ses liens ont toujours besoin de leur propre protection.

Les lecteurs respectent la même règle

Une recherche prend le mutex de la liste avant de lire head et le conserve pendant le parcours. Cela la coordonne avec l’insertion et la suppression. Définis une variable de résultat avant la boucle, mets-la à jour en cas de correspondance et sors de la boucle vers un point de déverrouillage unique avant le retour. Sinon, un retour anticipé en cas de succès dans la boucle verrouillée nécessiterait sa propre libération.

Dans cette conception, la suppression détient le même mutex pendant qu’elle retire un nœud de la chaîne. Les autres parcours ne peuvent pas se trouver dans ce nœud, car ils ont aussi besoin du mutex. Ne le libère que lorsqu’aucune autre opération ne peut encore y accéder. Si une API permet à un appelant de conserver un pointeur vers un nœud après le déverrouillage, cette utilisation supplémentaire nécessite sa propre règle de durée de vie ; le verrou de la liste ne la couvre plus à lui seul.

Déterminer si les verrous par nœud sont avantageux

Le verrouillage par couplage acquiert le verrou du nœud suivant avant de libérer celui du nœud courant. Ce chevauchement préserve un chemin protégé pendant la progression du parcours. Plusieurs opérations peuvent alors occuper différentes zones de la liste.

La concurrence supplémentaire a un prix : chaque nœud visité ajoute des opérations de verrouillage et de déverrouillage, même lorsqu’aucun autre fil n’est à proximité. Une charge comportant beaucoup de parcours peut devenir plus lente que la version à mutex unique. Verrouiller des groupes de nœuds est une solution intermédiaire. Compare la conception plus compliquée à la conception simple sous la charge réelle, recherches infructueuses et chemins de suppression compris.

Points clés

  • Protège ensemble la lecture de l’ancienne tête et la publication de la nouvelle.
  • Alloue et initialise les données privées du nœud avant de verrouiller la liste.
  • La recherche et la suppression utilisent le même verrou de protection que l’insertion.
  • Dans le verrouillage par couplage, acquiers le verrou du nœud suivant avant de libérer celui du nœud courant.

5.3 Sépare les extrémités des files et les alvéoles de hachage

Un mutex par structure constitue un bon point de départ, mais il peut sérialiser des opérations qui touchent des zones différentes. Deux améliorations utiles consistent à utiliser un verrou par extrémité de file et un verrou par alvéole de hachage.

Garder une sentinelle en tête de file

Une file à deux verrous commence avec un nœud sentinelle D et head == tail == D. D ne contient aucun élément courant de la file. Le premier élément réel, s’il existe, se trouve à head->next. Un mutex de tête sérialise les retraits ; un mutex de queue sérialise les ajouts. Un ajout et un retrait peuvent donc progresser ensemble.

L’ajout prépare de manière privée la valeur d’un nouveau nœud et son lien suivant nul. Sous le mutex de queue, il publie ce nœud par le champ next de l’ancienne queue et déplace tail vers le nouveau nœud. Le retrait utilise le mutex de tête pour lire le lien suivant de l’ancienne sentinelle. S’il est nul, il déverrouille et indique que la file est vide. Sinon, il copie la première valeur réelle, déplace head vers ce nœud, déverrouille et libère l’ancienne sentinelle.

Le nœud qui contenait la valeur renvoyée devient la sentinelle suivante. Le libérer immédiatement laisserait head pendant.

Suivre le déplacement de la sentinelle

Pars de D. L’ajout de 5 crée N5 et établit le lien D -> N5, avec tail == N5. L’ajout de 8 ajoute N8, ce qui produit D -> N5 -> N8. Un retrait renvoie 5, déplace head vers N5 et libère D. N5 est maintenant la sentinelle ; seule la valeur 8 reste dans la file. Après un autre retrait, N8 devient la sentinelle et la file est vide.

Les deux extrémités partagent toujours un lien

Lorsque la file est vide, l’ajout peut écrire le champ next de la sentinelle courante pendant que le retrait le lit sous un autre mutex. Les deux mutex seuls ne synchronisent pas ces accès. En C11, déclare next comme pointeur atomique et publie-le et charge-le avec un ordre approprié ; les stockages et chargements atomiques à cohérence séquentielle par défaut suffisent. La publication doit rendre visible la valeur préparée du nœud avant que le retrait ne la lise.

Attendre un élément nécessite un mécanisme de signalement en plus du verrouillage.

Hacher la clé, puis verrouiller l’alvéole

Une table de hachage de taille fixe peut utiliser une liste sûre vis-à-vis des fils dans chaque alvéole, chacune avec son propre mutex. Avec 101 alvéoles et bucket = key % 101, les clés 7 et 50 utilisent des verrous différents. Les clés 7 et 108 sont toutes deux associées à l’alvéole 7 et doivent y être sérialisées. Les collisions influent sur la contention autant que sur la longueur des recherches.

Tous les accès à la liste d’une alvéole suivent la règle de verrouillage de cette alvéole, y compris les lectures. Le redimensionnement modifie la correspondance avec les alvéoles et déplace les entrées ; cette conception simple nécessite donc un accès exclusif à toute la table pendant le redimensionnement. Un mutex d’alvéole seul ne peut pas protéger une modification qui touche toutes les alvéoles.

Affiner seulement après avoir trouvé le goulot d’étranglement

Commence par une carte de l’état partagé : quels champs sont partagés, quel mutex ou quelle opération atomique protège chacun et quand les nœuds deviennent accessibles ou peuvent être libérés. Vérifie chaque retour et chaque chemin d’échec d’allocation. Raisonne sur les entrelacements, teste sous charge avec un détecteur de courses et mesure la contention avant d’ajouter des verrous plus fins.

Points clés

  • Une sentinelle permet aux opérations sur la tête et la queue de la file d’utiliser des verrous distincts.
  • Un retrait réussi libère l’ancienne sentinelle et transforme le nœud contenant la valeur en nouvelle sentinelle.
  • Dans une file C11 à deux verrous, le lien partagé vers le nœud suivant doit être publié et lu de manière atomique.
  • Des verrous d’alvéole distincts permettent aux opérations dans différentes alvéoles de se chevaucher.
  • Le redimensionnement d’une table de hachage nécessite une coordination sur toute la table.
  • Améliore une conception correcte à l’aide de mesures de contention et de vérifications des durées de vie de la mémoire.

Fils d’exécution : exemples

5 Leçons Interactives

1

Suis un travailleur de sa création jusqu’à sa fin

14 étapes
Distingue la mémoire partagée de l’état des fils, transmets des arguments stables et recueille les résultats après la jonction.
  1. 1
    IntroductionCommence avec deux types d’état

    Un programme peut avoir plusieurs fils qui parcourent son code. Chaque exécution a besoin de sa propre instruction courante et de ses propres calculs inachevés, tandis que les fils peuvent tous accéder aux mêmes objets du programme.

    Suis d’abord ces deux types d’état. Construis ensuite une somme répartie entre deux travailleurs et détermine quand le fil principal peut lire les résultats. La sûreté de cette conception dépend de la durée de vie des objets et de la fin des travailleurs, pas d’une supposition sur celui qui s’exécute en premier.

  2. 2
    ExplicationDonne à chaque exécution sa propre position

    Un fil d’exécution possède son propre compteur ordinal, ses registres, sa pile et son identifiant. Sa pile stocke ses cadres de pile d’appels de fonction et ses variables locales ordinaires. Le système d’exploitation sauvegarde l’état d’exécution dans un TCB, ou bloc de contrôle de fil d’exécution.

    Lorsque l’ordonnanceur change de fil, il restaure l’état du nouveau fil. Un calcul dans un registre du premier fil reste inachevé jusqu’à la reprise de ce fil.

  3. 3
    ExempleRepère les objets partagés

    Supposons que total soit une variable globale et que chaque travailleur déclare une variable locale i. Tous les travailleurs accèdent à un seul total partagé, mais chaque appel de fonction possède son propre i.

    Les fils partagent aussi le tas et les descripteurs de fichier ouverts. Leurs piles se trouvent à des adresses différentes dans le même espace d’adressage du processus. Transmettre l’adresse de la variable locale d’un travailleur permet à un autre fil d’y accéder ; des piles distinctes ne garantissent pas l’isolation.

  4. 4
    LienFais se chevaucher le travail, même sur un cœur

    Sur un cœur de processeur, l’ordonnanceur entrelace les fils : ils progressent de manière concurrente, mais ne s’exécutent pas simultanément. Sur plusieurs cœurs, ils peuvent s’exécuter en parallèle.

    Les fils peuvent accélérer des calculs indépendants ou faire se chevaucher une attente d’entrée-sortie et un travail utile. Une commutation au sein d’un processus conserve l’espace d’adressage et les tables des pages ; elle coûte donc généralement moins cher qu’une commutation entre processus. La mémoire partagée permet aussi à une écriture invalide d’endommager les données des autres fils.

  5. 5
    ExplicationObserve ce que le noyau peut ordonnancer

    Dans un modèle un à un, chaque fil applicatif est un fil ordonnancé par le noyau. L’ordonnanceur peut les placer sur différents cœurs, et un appel système bloquant arrête le fil appelant. Linux et Windows utilisent ce modèle pour les fils ordinaires.

    Une bibliothèque plusieurs à un exécute des fils utilisateur sur un seul fil noyau. Ces fils utilisateur ne peuvent pas s’exécuter sur plusieurs cœurs, et un seul appel système bloquant peut arrêter leur groupe.

  6. 6
    DéfiUn cœur supplémentaire crée-t-il le bogue ?

    Un travailleur s’interrompt après avoir chargé des données partagées, et un autre les modifie avant la reprise du premier. Cela pourrait-il se produire sur un seul cœur ?

    ---reveal---

    Oui. L’ordonnanceur peut effectuer une commutation entre le chargement et l’utilisation ultérieure de la valeur. L’exécution parallèle peut révéler davantage de chevauchements, mais la concurrence seule permet cet ordonnancement. Affirmer qu’une exécution sur un cœur est sûre suppose de tenir compte des commutations entre instructions machine.

  7. 7
    ExplicationDémarre une fonction avec un pointeur

    Inclus <pthread.h> et compile avec -pthread. Un travailleur a la signature void *worker(void *arg). pthread_create(&t, NULL, worker, arg) le démarre, demande les attributs par défaut et stocke son identifiant de fil dans t.

    L’appel renvoie directement 0 en cas de succès ou un numéro d’erreur. Vérifie ce résultat avant de considérer le fil comme créé. Le nouveau fil peut s’exécuter immédiatement ; l’initialisation doit déjà être terminée.

  8. 8
    ExempleDivise la somme en tâches distinctes

    Prépare deux structures contenant lo, hi et result : l’une couvre les valeurs de 1 à 50 et l’autre de 51 à 100. Transmets une adresse de structure différente à chaque travailleur.

    Chaque travailleur additionne les valeurs de son intervalle dans un accumulateur local privé, puis écrit son propre champ de résultat. Le premier obtient 1 275 et le second 3 775. Aucun travailleur n’écrit le résultat de l’autre : la conception évite donc les mises à jour concurrentes d’un même accumulateur.

  9. 9
    ExempleAttends avant de lire les résultats

    Crée les deux travailleurs avant d’effectuer la jonction de l’un ou de l’autre, afin que leurs calculs puissent se chevaucher. Le fil principal appelle ensuite pthread_join pour chaque création réussie. Une jonction attend la fin du fil visé et rend ses écritures visibles pour le fil qui effectue la jonction.

    Le fil principal peut maintenant lire les deux champs de résultat distincts et les additionner pour obtenir 5 050. La jonction d’un seul travailleur ne permettrait pas d’établir que l’autre a terminé son propre résultat.

  10. 10
    ConseilGarde les arguments et les résultats en vie

    Un pointeur d’argument ne prolonge pas la durée de vie d’un objet. Garde les structures appartenant au créateur en vie jusqu’à la fin des travailleurs. Un pointeur renvoyé peut désigner de la mémoire du tas ou une zone mémoire adaptée appartenant au créateur ; il ne peut pas pointer de manière sûre vers une variable locale d’un travailleur terminé.

    Dans une boucle de création, transmets des éléments de tableau distincts et stables au lieu de transmettre &i à répétition. Cette dernière méthode fait pointer tous les travailleurs vers la même variable de boucle changeante.

  11. 11
    DéfiQuel résultat est prêt ?

    Deux travailleurs écrivent dans des champs de résultat distincts. Le fil principal effectue avec succès la jonction du travailleur A, puis lit les deux champs alors que le travailleur B peut encore s’exécuter. Toute cette lecture est-elle sûre ?

    ---reveal---

    Le résultat de A est prêt et visible après la jonction. Le champ de B peut encore changer : dans cette conception, le fil principal doit donc aussi effectuer la jonction de B avant de le lire. Des zones mémoire distinctes empêchent les interférences entre travailleurs ; la jonction établit la transmission de chaque travailleur au fil principal.

  12. 12
    ConseilDistingue la fin d’un fil de celle de son processus

    Le retour de main ou l’appel de exit() termine tout le processus, y compris les travailleurs qui n’ont pas fini. Le fil principal effectue normalement la jonction des travailleurs nécessaires avant de se terminer ; pthread_exit() ne termine que le fil appelant.

    Un travailleur détaché libère automatiquement ses ressources de fil lorsqu’il se termine et ne peut pas faire l’objet d’une jonction. Le détachement ne maintient pas le processus en vie et ne donne pas au travailleur une copie privée des objets d’argument du créateur.

  13. 13
    DéfiTermine le programme au bon moment

    Le fil principal démarre deux travailleurs qui lisent son tableau local, en détache un et effectue la jonction de l’autre. Le travailleur détaché s’exécute encore. Sa pile distincte lui permet-elle de finir d’utiliser le tableau après le retour de main ?

    ---reveal---

    Non. Le retour de main termine tout le processus, y compris ce travailleur. Sa pile distincte ne copie pas non plus le tableau : le pointeur donne accès à l’objet du fil principal dans leur espace d’adressage partagé. La jonction d’un travailleur n’établit que la fin de ce travailleur.

  14. 14
    RécapitulatifVérifie la transmission des résultats des travailleurs

    Pour une conception utilisant des travailleurs, localise l’état d’exécution, les objets partagés et la durée de vie de chaque argument et résultat. Identifie ensuite l’événement qui rend un résultat prêt pour son lecteur.

    Des objets de résultat distincts et des jonctions peuvent éviter complètement les verrous. Des mises à jour partagées pendant que les travailleurs s’exécutent simultanément nécessitent une coordination supplémentaire. L’ordre de création, les piles distinctes et le détachement répondent chacun à une question différente ; aucun n’établit qu’un autre fil a terminé ses écritures.

2

Suis l’opération qui doit être protégée

11 étapes
Suis une mise à jour perdue, distingue deux types de concurrence et protège une décision avec son action.

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

  1. 1
    IntroductionDécompose l’opération partagée
  2. 2
    ExplicationDistingue la mémoire des registres privés
  3. 3
    ExempleInterromps-toi avant le stockage
  4. 4
    ExempleReprends avec le résultat périmé
  5. 5
    DéfiChange le moment de l’ordonnancement
  6. 6
    ConseilChoisis une correction par synchronisation
  7. 7
    ExplicationDistingue les deux types de concurrence
  8. 8
    ExempleApprouve deux fois un retrait
  9. 9
    Point cléDélimite la section complète
  10. 10
    DéfiSuis ensemble l’approbation et la décrémentation
  11. 11
    RécapitulatifDélimite la section avant de choisir le verrou
3

Examine une règle de protection par mutex

14 étapes
Utilise un verrou partagé unique, choisis sa portée et suis chaque chemin de succès et d’erreur jusqu’à sa libération.

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

  1. 1
    IntroductionRends la règle explicite
  2. 2
    ExplicationInitialise avant de publier l’objet
  3. 3
    ExemplePlace toute la mise à jour à l’intérieur
  4. 4
    DéfiLis le résultat d’une tentative de verrouillage non bloquante
  5. 5
    ConseilChoisis délibérément le type de mutex
  6. 6
    ExplicationLaisse les zones indépendantes se chevaucher
  7. 7
    ExempleCalcule le plafond dû à la sérialisation
  8. 8
    DéfiRaccourcis la bonne partie du travail
  9. 9
    Point cléMesure trois résultats distincts
  10. 10
    ExempleSuis le retour anticipé
  11. 11
    ExplicationGarde un seul verrou et une durée de vie adaptée
  12. 12
    DéfiRepère un cycle dû à un rappel
  13. 13
    DéfiChoisis une correction complète après examen
  14. 14
    RécapitulatifLis le code en suivant la possession
4

Construis la prise de possession, des tours équitables et une attente efficace

21 étapes
Passe des tests d’indicateur défaillants aux verrous atomiques, puis compare l’ordre des tickets, les protocoles de mise en sommeil et l’héritage de priorité.

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

  1. 1
    IntroductionConstruis le verrou par étapes
  2. 2
    ExempleDeux appelants passent le même test
  3. 3
    ExplicationL’ordre fait partie de l’algorithme
  4. 4
    DéfiLe contrôle des interruptions protégerait-il deux cœurs ?
  5. 5
    ExplicationÉchange l’indicateur de manière atomique
  6. 6
    ExempleCrée un indicateur portable pour l’attente active
  7. 7
    ExempleUne comparaison échouée fournit une information utile
  8. 8
    ExplicationConnais les résultats des autres opérations atomiques
  9. 9
    DéfiRéessaie la mise à jour avec une valeur actualisée
  10. 10
    Point cléL’exclusion ne garantit pas un tour
  11. 11
    AnalogieUtilise une file avec des tickets numérotés
  12. 12
    ExempleFais avancer le service d’un possesseur à la fois
  13. 13
    DéfiDistingue le distributeur du service
  14. 14
    ExplicationLa cession du processeur et la mise en sommeil changent des états différents
  15. 15
    ExempleRepère la fenêtre où un réveil peut être perdu
  16. 16
    ExplicationCompare et bloque en une seule décision
  17. 17
    DéfiUn réveil arrive avant l’attente
  18. 18
    LienAttends activement un court moment si le possesseur peut finir
  19. 19
    ExempleObserve l’inversion de priorité du point de vue du possesseur
  20. 20
    DéfiCombine l’ordonnancement avec le verrou
  21. 21
    RécapitulatifDemande-toi ce qu’attend le fil
5

Suis les compteurs, les nœuds et les alvéoles partagés

16 étapes
Étudie les compteurs à transfert par lots, la publication dans une liste, le déplacement de la sentinelle d’une file et le verrouillage par alvéole.

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

  1. 1
    IntroductionRepère la limite de la partie partagée
  2. 2
    ExplicationRépartis les écritures fréquentes sur le compteur
  3. 3
    ExempleTransfère un lot complet
  4. 4
    Point cléBorne les incréments manquants
  5. 5
    DéfiChoisis le degré d’actualisation pour une nouvelle charge
  6. 6
    ConseilArrête les transferts pour obtenir un instantané exact
  7. 7
    ExemplePublie le nouveau nœud de liste en dernier
  8. 8
    ExplicationLes lecteurs et les fils qui suppriment partagent le verrou
  9. 9
    DéfiProgresse avec des verrous par nœud
  10. 10
    ExempleInitialise la file sans élément réel
  11. 11
    ExempleDéplace la sentinelle pendant le retrait
  12. 12
    Point cléLe cas d’une file vide réunit les deux extrémités
  13. 13
    DéfiSuis la sentinelle suivante
  14. 14
    LienSépare les alvéoles de hachage et préserve la correspondance
  15. 15
    DéfiCombine un verrou d’alvéole avec une insertion sûre
  16. 16
    RécapitulatifMesure la conception complète

Fils d’exécution : QCM

40 Questions Pratiques en 4 séries

Série 1 : Durée de vie des fils et mises à jour partagées10 Questions Pratiques

Question 1 sur 10

Deux fils appartiennent au même processus. Quel état d’exécution chacun conserve-t-il séparément ?
  1. Son code exécutable et son tas
  2. Son compteur ordinal et ses registres
  3. Ses fichiers ouverts et son identifiant de processus
  4. Ses variables globales et ses tables des pages
Afficher la réponseMasquer la réponse
Réponse : B
Chaque fil a besoin de son propre compteur ordinal et de ses propres registres afin de reprendre son exécution inachevée. Le tas appartient à l’espace d’adressage partagé : des chemins d’exécution distincts ne donnent donc pas aux fils des objets du tas distincts. Un objet devient privé par la manière dont il est utilisé, pas simplement parce qu’un fil l’a alloué.

Question 2 sur 10

Un serveur à un seul cœur passe d’un fil de traitement de requête à un autre pendant que l’un attend une réponse réseau. Comment décrire leur exécution ?
  1. Parallèle, sans progression concurrente
  2. Concurrente, avec exécution parallèle
  3. Séquentielle, sans progression qui se chevauche
  4. Concurrente, sans exécution parallèle
Afficher la réponseMasquer la réponse
Réponse : D
Les requêtes progressent pendant des périodes qui se chevauchent : c’est de la concurrence. Un cœur n’exécute qu’un seul fil à un instant donné, donc les fils ne s’exécutent pas en parallèle. Faire se chevaucher une attente réseau et un travail processeur ne nécessite pas que deux fils s’exécutent simultanément.

Question 3 sur 10

Un travailleur renvoie un pointeur vers un résultat alloué. Quel appel attend le fil t et place ce pointeur dans void *result ?
  1. pthread_join(t, &result)
  2. pthread_join(t, result)
  3. pthread_create(&t, NULL, result, NULL)
  4. pthread_detach(t)
Afficher la réponseMasquer la réponse
Réponse : A
Le second argument de la jonction est l’adresse de la variable qui recevra le pointeur renvoyé. Transmettre result lui-même fournit sa valeur actuelle de pointeur au lieu de l’emplacement de réception. Le détachement permet la récupération automatique des ressources mais supprime la possibilité d’effectuer une jonction et de recueillir cette valeur de retour.

Question 4 sur 10

Dans un modèle de chargement, addition et stockage, un compteur commence à 36. A et B le chargent chacun, ajoutent chacun 1 en privé, puis A et B stockent leur résultat dans cet ordre. Quelle est la valeur finale du compteur ?
  1. 36
  2. 38
  3. 37
  4. 39
Afficher la réponseMasquer la réponse
Réponse : C
Les deux calculs privés partent de 36, donc les deux stockages écrivent 37. Le second stockage écrase la même valeur ; il n’effectue pas une autre addition en mémoire partagée. Une valeur finale de 38 nécessiterait qu’un fil charge la valeur après le stockage de l’autre, ce que cet ordonnancement exclut.

Question 5 sur 10

Un solde atomique commence à 90. Deux fils le lisent chacun de manière atomique, approuvent une demande de 60, puis effectuent chacun une soustraction atomique de 60. Quelle défaillance reste possible ?
  1. Une approbation est perdue, laissant un solde de 30.
  2. Les accès atomiques forment une course aux données en C.
  3. Les deux approuvent, laissant un solde de −30.
  4. Les deux soustractions fusionnent en une seule mise à jour.
Afficher la réponseMasquer la réponse
Réponse : C
Les deux vérifications peuvent voir 90 avant l’une ou l’autre soustraction : les deux demandes sont donc approuvées et les deux soustractions atomiques laissent −30. Les opérations atomiques évitent une course aux données et préservent les deux soustractions. Elles ne maintiennent pas la validité de la vérification antérieure du solde jusqu’à la fin de l’action qui en dépend.

Question 6 sur 10

Un appel à pthread_mutex_trylock(&m) renvoie EBUSY. Quelle possession le fil appelant obtient-il par cet appel ?
  1. Il n’a acquis aucune possession de m.
  2. Il possède m jusqu’à son prochain blocage.
  3. Il partage m avec son détenteur actuel.
  4. Il possède m dès que son détenteur le déverrouille.
Afficher la réponseMasquer la réponse
Réponse : A
Le code indiquant que le mutex est occupé signifie que la tentative d’acquisition a échoué : l’appelant ne possède donc pas le mutex et ne peut pas entrer dans la section protégée. La tentative de verrouillage non bloquante ne réserve pas une acquisition future. Le fil peut effectuer un travail indépendant ou tenter à nouveau l’acquisition plus tard.

Question 7 sur 10

Une fonction de lancement démarre deux travailleurs en transmettant à répétition &i, l’adresse de son indice de boucle local, puis se termine pendant qu’ils s’exécutent. Le fil principal veut un résultat distinct de chacun. Quelle autre conception sécurise la transmission des arguments et des résultats ?
  1. Le fil principal garde en vie une structure de tâche changeante, démarre les deux travailleurs, puis effectue la jonction de chacun avant de lire son résultat.
  2. La fonction de lancement garde deux structures de tâche locales, démarre les deux travailleurs et se termine ; le fil principal effectue ensuite leur jonction avant la lecture.
  3. Le fil principal garde en vie deux structures de tâche distinctes, démarre les deux travailleurs, puis lit les résultats avant d’effectuer leur jonction.
  4. Le fil principal garde en vie deux structures de tâche distinctes, démarre les deux travailleurs, puis effectue la jonction de chacun avant de lire son résultat.
Afficher la réponseMasquer la réponse
Réponse : D
Chaque travailleur a besoin d’un objet d’argument distinct et stable, et les deux objets doivent survivre à leur utilisation par les travailleurs. Le fil principal doit ensuite effectuer la jonction de chaque travailleur avant de lire le résultat qu’il peut encore être en train d’écrire. La jonction ne peut pas réparer un objet local dont la durée de vie est déjà terminée, ni le fait que deux travailleurs reçoivent le même argument changeant.

Question 8 sur 10

Un programme perd des incréments partagés sur un seul cœur. Pourquoi l’ajout de volatile au compteur peut-il ne pas résoudre le problème ?
  1. Le qualificatif volatile oblige tous les fils à partager leurs registres.
  2. Une commutation de fil peut toujours séparer le chargement du stockage.
  3. Le compteur devient privé pour le fil en cours d’exécution.
  4. Le qualificatif reporte les commutations de fil après un chargement.
Afficher la réponseMasquer la réponse
Réponse : B
Volatile ne combine pas une lecture, un calcul et une écriture en une seule mise à jour atomique et ne synchronise pas les fils. Une commutation de contexte peut toujours placer une autre mise à jour entre le chargement et le stockage. Les registres utilisés pour le compteur sont privés à chaque fil : c’est précisément pourquoi le fil qui reprend peut conserver un résultat périmé.

Question 9 sur 10

Une capacité commence à 7. Des demandes de 3, 5 et 2 la vérifient chacune sous un mutex avant toute décrémentation et sont donc toutes approuvées. Elles décrémentent ensuite la capacité sous ce mutex dans le même ordre. Quelle valeur finale obtient-on et quelle délimitation du mutex corrige la règle ?
  1. Valeur finale 2 ; conserver des périodes de possession distinctes pour la vérification et la décrémentation.
  2. Valeur finale −3 ; conserver la possession pendant chaque vérification et sa décrémentation conditionnelle.
  3. Valeur finale −3 ; garder les périodes distinctes mais rendre aussi chaque accès atomique.
  4. Valeur finale −1 ; ne conserver la possession que pendant chaque décrémentation déjà approuvée.
Afficher la réponseMasquer la réponse
Réponse : B
Les trois demandes sont toutes approuvées à partir de 7 : leurs décrémentations ultérieures laissent donc 4, puis −1, puis −3. Une possession continue pendant chaque vérification et la décrémentation qui en dépend empêche les approbations fondées sur une valeur périmée. Des accès individuels atomiques ou des soustractions déjà protégées ne ferment pas l’intervalle entre la décision et l’action.

Question 10 sur 10

T1 possède un mutex pendant qu’il modifie deux champs liés, puis subit une préemption. T2 appelle pthread_mutex_lock sur le même mutex. Que se passe-t-il pour l’accès protégé ?
  1. T2 attend l’acquisition ; T1 peut reprendre et finir sa mise à jour.
  2. T2 entre parce que T1 a cessé de s’exécuter sur le processeur.
  3. T2 prend possession du mutex et termine la mise à jour inachevée de T1.
  4. T1 perd la possession du mutex et doit recommencer les deux modifications.
Afficher la réponseMasquer la réponse
Réponse : A
La possession survit à la préemption : T2 ne peut donc pas entrer simplement parce que T1 ne s’exécute plus sur le processeur. T1 peut reprendre en détenant toujours le mutex, terminer la mise à jour des deux champs et le libérer. Un mutex contrôle l’entrée dans le code protégé ; il n’exige pas que son possesseur s’exécute sans interruption.
Série 2 : Délimitation des mutex et prise de possession atomique10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

Un mutex protège toutes les alvéoles d’une table. De quel type de granularité de verrouillage s’agit-il ?
  1. Verrouillage à grain fin
  2. Verrouillage par couplage
  3. Verrouillage à gros grain
  4. Verrouillage par cœur
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Une recherche dans une liste verrouille son mutex, effectue immédiatement un retour en cas de correspondance et ne déverrouille qu’après la boucle si la recherche échoue. Pourquoi les opérations suivantes peuvent-elles se bloquer ?
  1. Une recherche réussie effectue un retour en conservant le mutex.
  2. Une recherche infructueuse libère le mutex avant de lire la tête.
  3. Une recherche réussie détruit automatiquement le mutex du nœud.
  4. Un résultat renvoyé transfère la possession du mutex à l’appelant.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Dans un protocole à indicateur, A observe que l’indicateur est à zéro et s’interrompt avant de le positionner. B le voit à zéro, le positionne et entre. Que peut-il se passer lorsque A reprend ?
  1. A doit tester à nouveau l’indicateur avant son affectation en attente.
  2. A positionne l’indicateur et entre aussi, ce qui viole l’exclusion.
  3. L’affectation de B modifie la valeur que A a déjà observée.
  4. La commutation de contexte de A annule son test de disponibilité achevé.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un échange atomique stocke 1 dans un indicateur qui vaut actuellement 0. Quelle valeur le test et positionnement renvoie-t-il ?
  1. 1
  2. L’adresse de l’indicateur
  3. Le ticket du fil
  4. 0
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un service à 16 cœurs utilise un mutex par compte. Un transfert prend d’abord le mutex du compte source, puis celui du compte destinataire. Des transferts concurrents peuvent avoir lieu dans des sens opposés. Quelle règle permet aux comptes indépendants de rester concurrents tout en corrigeant le problème d’ordre des verrous ?
  1. Acquérir le mutex source en premier et le libérer avant de verrouiller le destinataire.
  2. Acquérir le mutex destinataire en premier pour chaque transfert dans les deux sens.
  3. Acquérir un mutex global pour chaque opération sur chaque compte.
  4. Acquérir les deux mutex de compte par identifiant de compte croissant.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Une boucle de comparaison et échange lit un compteur à 12 et prépare 13. Un autre fil fait passer le compteur à 13 avant la comparaison et échange. Comment le premier fil préserve-t-il son incrément ?
  1. Répéter la même tentative avec la valeur attendue 12 jusqu’à sa réussite.
  2. Réessayer avec la nouvelle valeur du compteur et tenter de stocker 14.
  3. Stocker le 13 préparé sans vérifier à nouveau le compteur.
  4. Considérer la tentative échouée comme un incrément déjà effectué.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Un verrou à tickets a turn == 6 et pour prochain ticket inutilisé ticket == 9. Un nouvel appelant prend un ticket. Quel numéro attend-il ?
  1. 9
  2. 6
  3. 8
  4. 10
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Des travailleurs copient une structure de registre contenant un mutex et un pointeur vers des enregistrements partagés. Ils verrouillent leurs copies, effectuent un retour anticipé en cas d’échec de validation et lisent l’état avec des mutex locaux distincts. Quelle correction établit une règle de protection unique et libère le mutex en cas d’échec ?
  1. Utiliser le registre d’origine par pointeur, protéger les écritures avec son mutex et laisser les lecteurs garder leurs verrous locaux privés.
  2. Initialiser chaque mutex copié, protéger les lectures et écritures avec cette copie et faire passer les échecs par son déverrouillage.
  3. Utiliser le registre d’origine par pointeur, protéger les lectures et écritures avec son mutex et faire passer les échecs par le déverrouillage.
  4. Utiliser le registre d’origine par pointeur, protéger les lectures et écritures avec son mutex et effectuer un retour direct en cas d’échec.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Deux fils exécutent le protocole de Peterson. Les écritures de leurs propres indicateurs restent dans des tampons, si bien que leurs lectures ultérieures voient chacune l’autre fil comme non intéressé. Seul turn est atomique ; les indicateurs sont des entiers C ordinaires. Quelle conclusion et quelle correction en découlent ?
  1. Un seul peut entrer ; le tour atomique suffit à préserver tout le protocole.
  2. Les deux peuvent entrer ; utiliser des accès atomiques correctement ordonnés pour les indicateurs et le tour.
  3. Les deux peuvent entrer ; déclarer seulement les indicateurs ordinaires volatile corrige l’ordre.
  4. Un seul peut entrer ; des lignes de cache distinctes éliminent les courses sur les indicateurs ordinaires.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Chaque mise à jour d’une paire de champs partagés utilise le mutex m, mais une fonction de diagnostic lit les deux champs sans m. Que doit faire cette fonction pour obtenir une paire cohérente et protégée ?
  1. Déclarer ses copies locales volatile.
  2. Détenir un mutex privé pendant les lectures.
  3. Détenir m pendant la lecture des deux champs.
  4. Lire deux fois le premier champ sans verrou.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Série 3 : Protocoles d’attente et structures partagées10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

Un fil attendant un verrou dort dans une file d’attente du noyau. Combien de temps processeur consomme-t-il tant qu’il reste bloqué ?
  1. Une tranche de temps entière à chaque cycle de l’ordonnanceur.
  2. Un cœur en continu jusqu’à la libération par le possesseur.
  3. Une courte attente active à chaque accès mémoire protégé.
  4. Aucun pour l’exécution de ce fil bloqué.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

A détient le ticket 20 ; B et C détiennent 21 et 22. A libère le verrou, B n’est plus ordonnancé et C, qui s’exécute, continue d’attendre. D prend alors le ticket suivant, sans autre arrivée ensuite. Quel est le prochain ticket inutilisé et quel ordre d’entrée doit suivre lorsque B pourra s’exécuter ?
  1. Prochain ticket inutilisé : 23 ; B, puis C, puis D.
  2. Prochain ticket inutilisé : 24 ; C, puis D, puis B.
  3. Prochain ticket inutilisé : 24 ; B, puis C, puis D.
  4. Prochain ticket inutilisé : 22 ; B, puis D, puis C.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Un fil en attente décide de se mettre en sommeil, s’interrompt et reçoit un réveil avant d’appeler la mise en sommeil. Le réveil est ignoré. Quel risque en résulte ?
  1. Il peut dormir après que l’unique réveil a déjà eu lieu.
  2. Il peut acquérir deux tickets pour la même entrée dans la file.
  3. Il doit attendre activement jusqu’à la prochaine tranche de temps.
  4. Il se réveille immédiatement car la mise en sommeil mémorise toujours le réveil.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un compteur approché utilise trois emplacements locaux et un seuil de 4. En partant de zéro, leurs totaux de mises à jour sont 6, 1 et 3. Une fois toutes les mises à jour achevées, quels sont le décompte global et le total réel ?
  1. Décompte global 8 ; total réel 10.
  2. Décompte global 4 ; total réel 10.
  3. Décompte global 6 ; total réel 10.
  4. Décompte global 4 ; total réel 6.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Avant de chaîner un nœud de liste nouvellement alloué à une structure partagée, quel travail peut être effectué sans le mutex de la liste ?
  1. Lire la tête partagée pour une mise à jour ultérieure du lien.
  2. Modifier la tête partagée pour publier le nouveau nœud.
  3. Initialiser le champ de clé du nœud privé.
  4. Parcourir des nœuds partagés qu’une suppression peut libérer.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Une file contient la sentinelle R suivie de nœuds contenant 14 et 29. Après un retrait réussi, quel nœud est libéré et lequel devient la sentinelle ?
  1. Libérer le nœud contenant 14 ; R reste la sentinelle.
  2. Libérer R ; le nœud contenant 14 devient la sentinelle.
  3. Libérer R ; le nœud contenant 29 devient la sentinelle.
  4. Libérer le nœud contenant 29 ; R reste la sentinelle.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Une table de hachage de taille fixe utilise 17 alvéoles, key % 17 et un mutex par alvéole. Des travailleurs mettent à jour les clés 8, 25 et 26. Quelle paire doit être sérialisée et quelle clé utilise un verrou distinct ?
  1. 8 et 25 sont sérialisées ; 26 utilise un verrou distinct.
  2. 8 et 26 sont sérialisées ; 25 utilise un verrou distinct.
  3. 25 et 26 sont sérialisées ; 8 utilise un verrou distinct.
  4. Les trois sont sérialisées ; chacune utilise le même verrou d’alvéole.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Un compteur approché possède quatre emplacements locaux et utilise des incréments unitaires. Son retard après les mises à jour achevées ne doit jamais dépasser 20. Choisis le plus grand seuil entier respectant cette limite. Les emplacements effectuent ensuite 17, 8, 5 et 0 mises à jour en partant de zéro. Quels sont le seuil et la valeur globale obtenue ?
  1. Seuil 5 ; valeur globale 25.
  2. Seuil 7 ; valeur globale 21.
  3. Seuil 6 ; valeur globale 30.
  4. Seuil 6 ; valeur globale 18.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Un parcours avec verrouillage par couplage détient le verrou du nœud courant. Dans quel ordre doit-il passer au nœud suivant ?
  1. Libérer le verrou courant, puis acquérir le verrou suivant.
  2. Libérer le verrou courant, puis copier le pointeur suivant.
  3. Acquérir tous les verrous des nœuds suivants avant d’en libérer un.
  4. Acquérir le verrou suivant, puis libérer le verrou courant.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Sur un processeur unique avec un ordonnancement strict par priorité, L, de faible priorité, détient un verrou, H, de priorité élevée, l’attend activement et M, de priorité moyenne, est prêt. Le verrou est d’abord remplacé par un verrou à tickets à attente active. Il est ensuite remplacé par un mutex avec héritage de priorité sur lequel H se bloque. Quelle progression le second changement permet-il que l’ordre des tickets seul ne permet pas ?
  1. L s’exécute avant M, libère le verrou et laisse H progresser ; l’ordre des tickets seul laisse H empêcher L de s’exécuter.
  2. H entre avant L, libère le verrou et laisse M progresser ; l’ordre des tickets seul exige que M libère d’abord le verrou.
  3. M s’exécute avant L, libère le mutex de H et laisse H progresser ; l’ordre des tickets seul bloque toutes les priorités également.
  4. L reste moins prioritaire que M jusqu’à la fin de M ; l’ordre des tickets seul élèverait au contraire L à la priorité de H.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Série 4 : Examine les conceptions concurrentes dans leur ensemble10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

La valeur de retour d’un travailleur joignable ne sera jamais recueillie et ses ressources de fil doivent être récupérées lorsqu’il se termine. Quelle opération pthread permet cela ?
  1. Appeler pthread_join sans attendre sa fin.
  2. Détacher le travailleur avec pthread_detach.
  3. Effectuer un retour de main avant la fin du travailleur.
  4. Copier localement le pthread_t du travailleur.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

W observe un mot de verrou futex à 1. Le possesseur le remet à 0 et envoie un réveil avant que W appelle l’attente. C acquiert ensuite le verrou et met le mot à 1. W appelle l’attente sur futex avec 1 comme valeur attendue, puis C remet le mot à zéro et envoie un réveil. W peut-il se bloquer lors de cet appel et le réveil ultérieur lui donne-t-il la possession ?
  1. W peut se bloquer tant que C détient le verrou ; après son réveil, il doit réessayer de l’acquérir.
  2. W ne peut pas se bloquer car un possesseur précédent a déjà envoyé une notification de réveil.
  3. W peut se bloquer tant que C détient le verrou ; le réveil ultérieur lui donne automatiquement la possession.
  4. W doit se bloquer indéfiniment car sa lecture précédente concernait un autre possesseur.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Une file à deux verrous est vide avec D comme sentinelle. E prépare le nœud R avec la valeur 11, publie D->next = R et s’interrompt avant de déplacer la queue. Q effectue alors un retrait réussi ; E termine ensuite le déplacement de la queue vers R. Quel état reste-t-il et comment les accès au champ suivant entre les deux extrémités doivent-ils être implémentés en C11 ?
  1. Un élément 11 en R ; publier et lire le champ suivant de manière atomique avec un ordre adapté.
  2. File vide avec R comme sentinelle ; les mutex distincts des extrémités ordonnent les accès ordinaires au champ suivant.
  3. File vide avec D comme sentinelle ; des accès volatile au champ suivant assurent l’ordre de publication nécessaire.
  4. File vide avec R comme sentinelle ; publier et lire le champ suivant de manière atomique avec un ordre adapté.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Chaque requête détient un mutex partagé pendant 5 µs. En ignorant tous les autres coûts, quelle limite supérieure de débit ce travail protégé impose-t-il ?
  1. 5 000 requêtes par seconde.
  2. 1 000 000 de requêtes par seconde.
  3. 200 000 requêtes par seconde.
  4. 5 000 000 de requêtes par seconde.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un compteur possède un emplacement local à 5 et une valeur globale à 40. Un lecteur copie la valeur globale sous son verrou et le libère. Un travailleur transfère les 5 locaux au niveau global et remet la valeur locale à zéro. Le lecteur copie ensuite la valeur locale sous son verrou. Quel total calcule-t-il et comment peut-il obtenir un instantané exact ?
  1. Il calcule 40 ; détenir tous les verrous locaux puis le verrou global pour l’instantané.
  2. Il calcule 45 ; lire deux fois la valeur globale sans prendre de verrou local.
  3. Il calcule 50 ; verrouiller uniquement le compteur global pendant tout l’instantané.
  4. Il calcule 40 ; verrouiller brièvement chaque champ uniquement pendant sa copie.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Une mise à jour de cache détient un mutex normal et appelle un rappel qui interroge le même cache. Déplacer le rappel après le déverrouillage éviterait ce nouvel accès sous verrou, mais le nœud désigné par son pointeur brut pourrait alors être libéré par un autre travailleur. Quelle modification permet d’exécuter le rappel hors du verrou tout en gardant son entrée valide ?
  1. Conserver le pointeur brut vers le nœud, déverrouiller avant le rappel et laisser celui-ci acquérir le même mutex à son démarrage.
  2. Copier les champs nécessaires dans une zone mémoire appartenant au rappel sous le mutex, déverrouiller, puis appeler le rappel sur cette copie.
  3. Copier les champs dans une zone mémoire appartenant au rappel, garder le mutex détenu et laisser le rappel interroger le cache avec le même mutex.
  4. Conserver le pointeur brut vers le nœud, déverrouiller et déclarer le pointeur volatile pour qu’une suppression ultérieure ne puisse pas invalider le nœud.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Une écriture conditionnelle échoue après un chargement lié alors que le code ne dispose d’aucun indice de la présence d’un autre rédacteur. Comment la routine de mise à jour doit-elle réagir ?
  1. Considérer l’écriture conditionnelle échouée comme réussie.
  2. Écrire la valeur avec un stockage ordinaire inconditionnel.
  3. Réessayer la séquence LL/SC avec un nouveau chargement lié.
  4. Renvoyer l’ancienne valeur et indiquer que la mise à jour a été validée.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Deux recherches trouvent un nœud. A copie sa valeur entière dans une zone mémoire appartenant à l’appelant sous le mutex de la liste. B sauvegarde un pointeur et déverrouille. Une opération de suppression retire ensuite le nœud de la chaîne et le libère ; son adresse peut être réutilisée avant que B lise la valeur. Quel résultat reste valide et que doit fournir l’interface de B ?
  1. Les deux résultats restent valides ; B doit seulement verrouiller à nouveau la liste avant de lire l’adresse sauvegardée.
  2. Seul le pointeur de B reste valide ; A doit verrouiller la liste chaque fois qu’il lit la valeur copiée.
  3. La valeur copiée de A reste valide ; déclarer volatile le pointeur sauvegardé de B préserve le nœud supprimé.
  4. La valeur copiée de A reste valide ; B a besoin d’une copie de la valeur ou d’une protection qui maintient le nœud en vie.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Le prochain ticket inutilisé d’un verrou à tickets est 15 et son tour courant est 12. Trois appelants prennent des tickets avant toute libération. Quels sont leurs tickets et la nouvelle valeur du distributeur ?
  1. Tickets 15, 16, 17 ; prochain ticket inutilisé 18.
  2. Tickets 12, 13, 14 ; prochain ticket inutilisé 15.
  3. Tickets 16, 17, 18 ; prochain ticket inutilisé 18.
  4. Tickets 15, 15, 15 ; prochain ticket inutilisé 18.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Une table passe de 11 à 17 alvéoles en utilisant la clé modulo le nombre d’alvéoles. Un travailleur a sélectionné l’ancienne alvéole pour la clé 24. La fonction de redimensionnement ne verrouille que la nouvelle alvéole de destination, déplace l’entrée et libère l’ancien tableau d’alvéoles. Quelle destination et quelle règle de transition préservent cette conception simple ?
  1. Alvéole 2 ; garder l’ancienne correspondance tout en publiant le nouveau nombre d’alvéoles pour les opérations ordinaires.
  2. Alvéole 7 ; protéger uniquement la nouvelle destination puisque le travailleur a calculé son ancien indice auparavant.
  3. Alvéole 7 ; exclure les opérations ordinaires sur la table et attendre la fin des anciens utilisateurs avant de retirer l’ancien stockage.
  4. Alvéole 2 ; protéger uniquement l’ancienne source tout en libérant chaque ancienne alvéole après le déplacement de l’entrée.
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

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

A

Algorithme de Peterson
L’algorithme de Peterson est un protocole d’exclusion pour deux fils utilisant des indicateurs d’intérêt et une variable de tour, correct en cohérence séquentielle.
Lorsque les deux fils demandent à entrer, la variable de tour détermine lequel des deux fils intéressés attend.
Alvéole de hachage
Une alvéole de hachage est une zone de table sélectionnée par la correspondance de hachage d’une clé et contenant les entrées associées à cet emplacement.
Le calcul modulo 101 place les clés 7 et 108 dans la même alvéole, bien que leurs valeurs numériques soient différentes.
Appel réentrant
Un appel réentrant entre à nouveau dans une opération avant la fin de son invocation précédente, et peut ainsi tenter d’acquérir à nouveau un verrou détenu.
Un rappel de journalisation peut interroger le même cache alors que sa mise à jour externe possède encore le mutex du cache.
Argument du fil d’exécution
Un argument du fil d’exécution est une donnée fournie à un nouveau travailleur par le paramètre pointeur de sa fonction d’entrée, qui pointe souvent vers une structure.
Un tableau donne à chaque travailleur un objet d’argument distinct qui reste valide jusqu’à la fin des jonctions.
Atomicité
L’atomicité signifie qu’une opération paraît indivisible aux observateurs concernés, qui ne peuvent donc pas observer son état intermédiaire partiel.
Un mutex partagé unique peut rendre atomique une mise à jour de deux champs pour les autres utilisateurs de ce même mutex.
Attente active
L’attente active consiste à tester une condition à répétition tout en restant prêt à s’exécuter et en consommant du temps processeur, jusqu’à ce que la condition permette de progresser.
Un fil en attente peut passer toute sa tranche de temps à vérifier un indicateur pendant que le possesseur ayant subi une préemption ne peut pas s’exécuter.
Attente bornée
L’attente bornée limite le nombre d’entrées concurrentes dans une section critique qui peuvent précéder un fil après sa demande d’entrée.
Les tickets antérieurs encore en cours bornent le nombre d’entrées avant le tour d’un fil attendant un verrou à tickets.

B

Bloc de contrôle de fil d’exécution
Un bloc de contrôle de fil d’exécution est un enregistrement du système d’exploitation qui contient l’état d’exécution sauvegardé d’un fil pour son ordonnancement et sa reprise.
Lors d’une commutation entre deux fils, l’état des registres du premier est sauvegardé dans son bloc de contrôle avant la restauration de l’état du second.

C

Cession du processeur
Une cession du processeur abandonne le tour courant de l’appelant tout en le laissant prêt à s’exécuter à nouveau lorsque l’ordonnanceur le choisit.
De nombreux fils attendant un verrou et cédant le processeur peuvent provoquer des commutations de contexte répétées avant que le possesseur obtienne un nouveau tour.
Chargement lié et écriture conditionnelle
Le chargement lié et écriture conditionnelle associe une lecture qui établit une réservation à une tentative d’écriture qui peut échouer et doit alors être réessayée.
Une réservation invalidée fait échouer l’écriture conditionnelle au lieu de laisser écraser une modification concurrente.
Chemin de nettoyage
Un chemin de nettoyage est une séquence de sortie commune qui libère les ressources acquises avant le retour d’une fonction, en cas de succès ou d’échec.
Une recherche enregistre sa valeur de retour, sort de la boucle et atteint un déverrouillage commun du mutex.
Cohérence séquentielle
La cohérence séquentielle donne aux opérations un ordre commun observé par tous les fils et préservant l’ordre du programme de chaque fil.
La démonstration de Peterson suppose que les indicateurs et le tour suivent cet ordre, sans réordonnancement de leur visibilité.
Collision de hachage
Une collision de hachage se produit lorsque des clés distinctes sont associées à la même alvéole de table et partagent ainsi une zone de recherche et son verrou.
Avec 13 alvéoles, les clés 2 et 15 entrent en collision car chacune donne un reste de 2 lorsqu’elle est divisée par 13.
Comparaison et échange
La comparaison et échange ne remplace une valeur de manière atomique que si elle correspond à une valeur attendue, et fournit assez d’informations pour détecter un échec.
Un fil qui met à jour un compteur réessaie lorsqu’un incrément concurrent rend sa valeur attendue périmée.
Compteur approché
Un compteur approché regroupe par lots des mises à jour locales protégées dans un décompte global protégé dont la valeur lue peut être en retard sur le total exact.
Un collecteur de statistiques peut tolérer que des incréments récents restent locaux jusqu’à ce que leur seuil de transfert soit atteint.
Compteur de tour
Un compteur de tour indique le ticket actuellement autorisé à entrer dans un verrou à tickets et avance une fois lorsque son détenteur libère le verrou.
Prendre un nouveau ticket fait avancer le distributeur tout en laissant le tour courant inchangé.
Compteur par cœur
Un compteur par cœur enregistre les mises à jour dans un emplacement associé à un cœur de processeur, réduisant les écritures vers un emplacement partagé global unique.
Les fils utilisant différents emplacements locaux peuvent les mettre à jour sous des mutex différents jusqu’à ce qu’un transfert par lot soit nécessaire.
Concurrence
La concurrence est la progression de plusieurs exécutions pendant des périodes qui se chevauchent, y compris leur exécution entrelacée sur un seul cœur de processeur.
Une horloge peut provoquer une commutation entre des travailleurs concurrents même lorsque la machine ne peut en exécuter qu’un à un instant donné.
Condition de concurrence
Une condition de concurrence se produit lorsque la justesse d’une opération dépend du moment relatif d’actions concurrentes.
Deux approbations protégées séparément peuvent toutes deux utiliser l’ancien solde avant l’application de l’un ou l’autre retrait.
Contention
La contention se produit lorsque des fils se disputent une ressource partagée, par exemple en tentant d’acquérir un mutex détenu par un autre fil.
Ajouter des travailleurs à un compteur protégé par un seul verrou peut augmenter l’attente et le trafic de cache partagé au lieu du débit.
Course aux données
Une course aux données est un accès conflictuel non synchronisé au même emplacement mémoire par différents fils, comprenant une écriture et un accès non atomique.
Une lecture d’état peut entrer en concurrence avec l’écriture d’un compteur ordinaire même si la fonction d’état ne modifie rien.
Création de fil d’exécution
La création de fil d’exécution démarre un nouveau chemin d’exécution à une fonction fournie, avec un pointeur d’argument et des attributs de fil configurables.
Le fil principal prépare une structure d’intervalle avant d’en transmettre l’adresse à un travailleur chargé d’une somme et créé avec succès.

D

Durée de vie d’un objet
La durée de vie d’un objet est la période pendant laquelle un objet stocké existe et peut être utilisé au moyen d’une référence par ailleurs valide.
Renvoyer un pointeur vers une variable locale d’un travailleur ne fonctionne pas, car l’existence de cet objet prend fin avec la fonction.

E

Entrelacement
Un entrelacement est un ordre dans lequel les étapes de différents fils se produisent tout en préservant l’ordre d’exécution pertinent au sein de chaque fil.
Placer la mise à jour complète d’un fil entre le chargement et le stockage d’un autre montre comment un résultat périmé peut l’écraser.
Espace d’adressage
Un espace d’adressage est l’ensemble des adresses mémoire disponibles pour un processus ; ses fils partagent le code et les objets qui s’y trouvent.
Les piles de deux travailleurs occupent différentes zones d’un même espace d’adressage, si bien qu’un pointeur peut passer de l’une à l’autre.
Exclusion mutuelle
L’exclusion mutuelle est la propriété selon laquelle un seul fil concurrent au plus occupe une section critique protégée à un instant donné.
Un second fil ne peut pas acquérir le mutex partagé tant qu’un premier le possède, même si ce possesseur subit temporairement une préemption.

F

Famine
La famine est l’attente indéfinie d’un fil tandis que des fils concurrents continuent de progresser et obtiennent à répétition la ressource dont il a besoin.
Un fil attendant un verrou fondé sur le test et positionnement peut perdre chaque course après le déverrouillage, car le verrou ne mémorise aucun ordre d’arrivée.
Fil d’exécution
Un fil d’exécution est une séquence d’exécution dans un processus, avec son propre compteur ordinal, ses registres, sa pile et son identité de fil.
Un serveur peut utiliser un fil pour attendre une réponse pendant qu’un autre traite une requête terminée.
Fil d’exécution bloqué
Un fil d’exécution bloqué ne peut pas être choisi pour s’exécuter tant qu’un événement ne le rend pas à nouveau prêt ; il n’utilise donc pas le processeur pendant son blocage.
Un fil en sommeil qui attend un mutex quitte l’ensemble des fils prêts jusqu’à ce qu’un réveil lui permette de concourir à nouveau.
Fil d’exécution détaché
Un fil d’exécution détaché libère automatiquement ses ressources de fil lorsqu’il se termine et ne peut pas faire l’objet d’une jonction pour recueillir sa valeur de retour.
Un travailleur en arrière-plan peut être détaché, mais le retour de main termine toujours le processus qui le contient.
File à deux verrous
Une file à deux verrous sérialise les ajouts avec un verrou de queue et les retraits avec un verrou de tête, permettant aux opérations sur les extrémités opposées de se chevaucher.
Dans une file C11 à deux verrous, le lien suivant de la sentinelle lorsque la file est vide doit toujours être publié et lu de manière atomique.
Fils d’exécution POSIX
Les fils d’exécution POSIX constituent l’interface normalisée que les programmes C utilisent sur les systèmes de type Unix pour créer et coordonner des fils.
Un programme C utilisant un travailleur inclut <pthread.h> et est compilé avec l’option -pthread, qui sert aussi à l’édition de liens.
Fuite de mémoire
Une fuite de mémoire est une zone mémoire allouée qui n’est plus nécessaire mais ne peut pas être récupérée parce que le programme a perdu sa référence utilisable.
Deux insertions concurrentes en tête peuvent écraser la tête et rendre inaccessible le premier nouveau nœud.
Futex
Un futex est un mot en mémoire utilisateur associé à des mécanismes du noyau Linux qui permettent de comparer sa valeur à une valeur attendue et de décider du blocage de manière atomique, ainsi que de réveiller les fils en attente.
Un mutex sans contention peut utiliser des opérations atomiques en espace utilisateur et n’appeler le noyau que pour une attente avec contention.

G

Granularité du verrouillage
La granularité du verrouillage est la quantité d’état partagé qu’un verrou protège ; elle détermine quelles opérations doivent être sérialisées entre elles.
Remplacer un mutex de table par des mutex par alvéole permet aux opérations dans différentes alvéoles de se chevaucher.

H

Héritage de priorité
L’héritage de priorité élève temporairement la priorité du possesseur d’un verrou à celle, plus élevée, d’un fil en attente afin qu’il puisse s’exécuter et libérer le verrou plus tôt.
Relever la priorité du possesseur empêche les tâches de priorité intermédiaire de retarder indéfiniment la libération nécessaire.

I

Identité du mutex
L’identité du mutex est liée à l’objet mutex d’origine à son adresse d’initialisation ; une copie d’un mutex pthread n’est pas un accès valide au même objet.
Transmets un registre partagé par pointeur au lieu de copier sa structure et d’utiliser le mutex copié.
Instantané exact
Un instantané exact lit des valeurs partagées liées de manière cohérente dans un même état protégé, empêchant les transferts de scinder l’observation.
Un instantané de compteur approché détient les verrous locaux et le verrou global pendant l’addition de tous les décomptes.
Inversion de priorité
Une inversion de priorité se produit lorsqu’une tâche de priorité élevée attend un possesseur moins prioritaire dont la progression est retardée par d’autres activités d’ordonnancement.
Des tâches de priorité moyenne peuvent empêcher un possesseur de mutex de faible priorité de le libérer pour un fil en attente de priorité élevée.

J

Jonction
Une jonction attend la fin d’un fil joignable et rend ses écritures achevées visibles pour le fil qui effectue la jonction avec succès.
Le fil principal effectue la jonction de deux travailleurs chargés de sommes avant de lire et de combiner leurs champs de résultat distincts.

L

Lecture et addition
La lecture et addition ajoute une quantité à une valeur stockée de manière atomique et renvoie la valeur qui existait avant l’addition.
Un distributeur de tickets renvoie 12 à un appelant tout en faisant passer son prochain numéro disponible à 13.
Lecture-modification-écriture
Une opération de lecture-modification-écriture lit une valeur stockée, calcule une valeur modifiée et réécrit le résultat au même emplacement partagé.
Un incrément ordinaire de compteur laisse un intervalle entre son chargement initial et son stockage final.

M

Mise à jour perdue
Une mise à jour perdue se produit lorsqu’un fil stocke un résultat fondé sur une ancienne lecture et écrase l’effet d’une mise à jour concurrente.
Deux travailleurs peuvent chacun charger 50 depuis un compteur partagé et stocker 51, ne laissant qu’un incrément visible après deux tentatives.
Modèle de fils d’exécution plusieurs à un
Le modèle de fils d’exécution plusieurs à un associe plusieurs fils utilisateur à un seul fil noyau, ce qui empêche leur exécution simultanée sur des cœurs différents.
Un appel système bloquant peut arrêter tous les fils utilisateur associés à ce seul chemin d’exécution du noyau.
Modèle de fils d’exécution un à un
Le modèle de fils d’exécution un à un associe chaque fil applicatif à un fil ordonnancé par le noyau, ce qui permet l’exécution parallèle et le blocage individuel des fils.
Deux fils Linux ordinaires peuvent être placés sur des cœurs distincts par l’ordonnanceur du système d’exploitation.
Mutex
Un mutex est un verrou d’exclusion mutuelle possédé par un seul fil à la fois et utilisé pour protéger une opération ou une zone de données partagée.
Toutes les opérations sur le solde acquièrent le même mutex de compte avant d’utiliser le champ de solde protégé.
Mutex avec vérification d’erreurs
Un mutex avec vérification d’erreurs signale certaines erreurs de possession, notamment un nouveau verrouillage par le possesseur et une libération par un fil qui ne le possède pas.
Pendant le développement, une tentative de nouveau verrouillage peut renvoyer EDEADLK au lieu de bloquer silencieusement le même fil.
Mutex récursif
Un mutex récursif permet des acquisitions répétées par son possesseur et ne devient disponible qu’après le nombre correspondant de libérations.
Deux acquisitions réussies par un même fil nécessitent deux déverrouillages avant qu’un autre fil puisse acquérir le mutex.

N

Nœud sentinelle
Un nœud sentinelle sert d’ancrage à une file chaînée sans représenter un élément courant ; son nœud suivant, s’il existe, contient la première valeur de la file.
Un retrait renvoie la valeur du nœud suivant, libère l’ancienne sentinelle et fait du nœud de valeur la nouvelle sentinelle.

O

Opération atomique sans verrou
Une opération atomique sans verrou produit son effet atomique sans dépendre de la libération d’un verrou interne par un autre fil.
D’autres objets atomiques peuvent utiliser un verrou de bibliothèque : la justesse atomique seule ne garantit donc pas l’absence de verrou.
Ordre des accès mémoire
L’ordre des accès mémoire précise les contraintes sur la manière dont les opérations mémoire deviennent visibles, y compris autour du passage de relais d’un verrou.
La libération d’un verrou doit maintenir les écritures protégées avant la publication du nouvel état disponible.
Ordre du premier arrivé, premier servi
L’ordre du premier arrivé, premier servi traite les demandes selon leur ordre d’arrivée enregistré, empêchant les arrivants ultérieurs enregistrés de dépasser les précédents.
Pour un verrou à tickets, l’arrivée est enregistrée lorsque l’appelant obtient son numéro de ticket de manière atomique.

P

Parallélisme
Le parallélisme est l’exécution simultanée sur différentes ressources de traitement, par exemple plusieurs fils s’exécutant sur des cœurs de processeur distincts.
Des parties indépendantes d’une image peuvent être traitées en parallèle lorsque plusieurs cœurs sont disponibles.
Possession du verrou
La possession du verrou est le droit exclusif obtenu par une acquisition réussie et conservé jusqu’à ce que le fil possesseur libère le mutex.
Une commutation de contexte interrompt le possesseur sans autoriser un fil en attente à entrer.
Progression
La progression exige que les fils qui cherchent à entrer dans une section critique libre puissent choisir un entrant sans obstruction indéfinie de la part des autres fils.
Un fil qui n’essaie pas d’entrer ne doit pas maintenir indéfiniment fermée une section autrement disponible.
Publication d’un nœud
La publication d’un nœud rend un nœud privé préparé accessible par des liens partagés afin que d’autres opérations puissent trouver ses données initialisées en toute sécurité.
Une insertion en tête renseigne d’abord la clé et ne publie la tête qu’en détenant le mutex partagé de la liste.

R

Rebouclage du compteur
Le rebouclage du compteur se produit lorsque l’arithmétique non signée dépasse la valeur maximale représentable et reprend à zéro.
La comparaison des tickets par égalité ne reste utilisable après un rebouclage que si les numéros des tickets en cours restent uniques.
Récupération de mémoire
La récupération de mémoire libère une zone mémoire allouée seulement lorsqu’aucune opération ne peut encore accéder à l’objet qui l’occupe.
Retirer un nœud de la chaîne ne suffit pas lorsqu’un appelant a conservé son pointeur et peut le déréférencer plus tard.
Redimensionnement de la table
Le redimensionnement de la table modifie l’organisation des alvéoles d’une table de hachage et réaffecte les entrées, ce qui nécessite une coordination au-delà du verrou ordinaire d’une seule alvéole.
Une table de hachage simple avec verrou par alvéole exclut les opérations concurrentes sur la table pendant le déplacement des entrées vers de nouvelles alvéoles.
Règle de protection
Une règle de protection associe un état partagé à un verrou particulier et précise quels accès doivent acquérir ce même verrou.
Un commentaire à côté d’un compteur peut nommer le mutex que chaque lecture et mise à jour concurrente doit acquérir.
Réveil perdu
Un réveil perdu se produit lorsqu’une notification nécessaire ne parvient pas à empêcher ou à terminer une attente, ce qui peut laisser un fil bloqué à tort.
Un fil utilisant un protocole simple peut se mettre en sommeil après qu’un réveil antérieur a été ignoré, sans qu’une autre libération puisse le réveiller.

S

Section critique
Une section critique est une portion de code qui accède à un état partagé et doit exclure les opérations concurrentes pour préserver la justesse de l’opération.
La section critique d’un retrait commence avant la vérification du solde et comprend la soustraction conditionnelle.
Seuil de transfert
Un seuil de transfert est le décompte local à partir duquel un compteur approché transfère ses incréments accumulés vers le décompte global.
Augmenter le seuil réduit les acquisitions du verrou global mais permet à davantage d’incréments de rester non reportés.
Sûreté vis-à-vis des fils d’exécution
La sûreté vis-à-vis des fils d’exécution signifie qu’une structure se comporte conformément à sa spécification lorsque les opérations de plusieurs fils s’entrelacent.
Un compteur a besoin d’une règle de protection pour les lectures comme pour les incréments lorsque ces opérations peuvent se chevaucher.

T

Tentative de verrouillage non bloquante (trylock)
Une tentative de verrouillage non bloquante est une tentative d’acquisition immédiate qui indique si l’appelant a obtenu le mutex au lieu d’attendre pendant qu’il est détenu.
Un résultat EBUSY permet à un travailleur de choisir un travail indépendant, mais ne lui donne pas accès aux champs protégés.
Test et positionnement
Le test et positionnement stocke une nouvelle valeur de manière atomique et renvoie l’ancienne, permettant à un appelant de prendre un indicateur libre sans intervalle.
Un échange qui reçoit 0 prend possession de l’indicateur ; les concurrents qui reçoivent 1 continuent d’attendre sa libération.
ThreadSanitizer
ThreadSanitizer est un outil d’analyse à l’exécution qui détecte les courses aux données dans des exécutions instrumentées et signale les accès conflictuels.
Une compilation de test utilisant -fsanitize=thread peut révéler un chemin de lecture qui a omis le mutex utilisé pour la mise à jour.

V

Verrou à attente active
Un verrou à attente active est un verrou d’exclusion mutuelle dont les appelants qui échouent vérifient ou tentent l’acquisition à répétition en consommant du temps processeur.
Une courte détention sur un autre cœur en cours d’exécution peut rendre l’attente active moins coûteuse que le blocage et le réveil.
Verrou à deux phases
Un verrou à deux phases attend d’abord activement pendant un court moment, puis bloque si le possesseur n’a pas libéré le verrou pendant cet intervalle.
La courte attente active peut éviter le surcoût du réveil pour une détention brève sans consommer un cœur pendant une longue détention.
Verrou à tickets
Un verrou à tickets donne à chaque demandeur un ticket atomique unique et lui permet d’entrer lorsque le compteur de service atteint ce ticket.
Un appelant détenant le ticket 6 attend tant que le tour est à 5, même si un appelant ayant un ticket ultérieur s’exécute déjà.
Verrou d’alvéole
Un verrou d’alvéole protège les entrées d’une alvéole de hachage, permettant aux opérations sur différentes alvéoles de progresser sous des verrous distincts.
Deux clés ayant le même indice d’alvéole sont en concurrence pour le même verrou malgré leurs valeurs différentes.
Verrouillage à grain fin
Le verrouillage à grain fin attribue des verrous distincts à des zones plus petites de l’état partagé, permettant aux opérations indépendantes de progresser simultanément.
Des mutex de compte distincts permettent de mettre à jour simultanément deux paires de comptes sans lien entre elles.
Verrouillage à gros grain
Le verrouillage à gros grain protège une grande structure ou un sous-système avec un seul verrou, ce qui simplifie la garantie de justesse mais limite les accès indépendants.
Un seul mutex de liste oblige chaque recherche à attendre une insertion, même lorsque sa cible se trouve loin de celle-ci.
Verrouillage par couplage
Le verrouillage par couplage acquiert le verrou du nœud suivant avant de libérer celui du nœud courant, préservant la protection pendant le parcours.
Différents parcours peuvent occuper des zones distinctes de la liste, mais chaque nœud visité ajoute un surcoût de verrouillage.
Volatile
Le qualificatif C volatile soumet les accès à des règles particulières d’accès observable ; il ne fournit ni atomicité ni synchronisation des fils.
Déclarer un compteur partagé volatile ne combine pas son chargement, son addition et son stockage en une seule mise à jour.

À 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