Spinlock vs mutex : les principales différences

Le spinlock et le mutex sont deux verrous qui ne laissent entrer qu’un seul thread à la fois dans une section critique, mais ils n’attendent pas de la même façon que le verrou se libère. Le spinlock attend activement en testant en boucle la variable du verrou : il continue d’utiliser le CPU pendant l’attente, mais prend le verrou dès qu’il se libère. Le mutex endort le thread en attente et libère le CPU ; lorsque le verrou est relâché, l’OS réveille le thread. On utilise le spinlock pour des durées de détention très courtes ou dans les gestionnaires d’interruption, qui ne peuvent pas dormir, et le mutex pour des détentions longues ou lorsque le thread risque de dormir pendant qu’il tient le verrou.

CritèreSpinlockMutex
Mode d’attenteTeste la variable en boucle jusqu’à ce que le verrou se libère (attente active)S’endort si le verrou est occupé et est réveillé lorsqu’il est libéré
CPU pendant l’attenteContinue de l’utiliserLe libère. D’autres threads peuvent utiliser le CPU entre-temps
Coût de l’attentePas de changement de contexte ; le verrou est pris dès qu’il se libèreL’endormissement et le réveil coûtent des appels système et des changements de contexte
Durée de détention adaptéeTrès courte (plus courte que la durée d’un changement de contexte)Longue. Lorsque le thread risque de dormir pendant la détention, par exemple en attendant une E/S
Dormir en tenant le verrouInterdit. Le côté en attente continuerait de consommer du CPUPossible
Avec un seul cœurPendant l’attente, le thread qui tient le verrou ne peut pas s’exécuter : tourner en boucle ne libère pas le verrou. Dans le noyau, on l’utilise en désactivant la préemptionFonctionne sans problème
Usage dans le noyauUtilisable aussi là où l’on ne peut pas dormir, comme dans les gestionnaires d’interruption (contexte d’interruption). Pour des données partagées avec une interruption, on désactive les interruptions avant de prendre le verrouRisque de dormir : utilisable uniquement en contexte de processus, pas dans les gestionnaires d’interruption
Inversion de prioritéSi le thread qui tient le verrou est interrompu, le côté en attente continue de tourner. Le noyau l’évite en désactivant la préemption pendant la détentionPeut être traitée par un mutex à héritage de priorité
Exemple d’implémentationLa variable est modifiée par des instructions atomiques (test-and-set ou compare-and-swap). Le test-and-test-and-set attend normalement par des lectures ordinaires, ce qui réduit les communications entre cœursAvec le futex de Linux, en l’absence de contention, le verrouillage se fait uniquement par des instructions atomiques en espace utilisateur ; ce n’est qu’en cas de contention que le noyau endort le thread. Certaines implémentations tournent un peu avant de dormir

Les valeurs indiquées sont des ordres de grandeur typiques et varient selon la génération de procédé et le produit.

Quand choisir l'un ou l'autre ?

Spinlock
Sur un système multicœur, lorsque le verrou est tenu très brièvement (moins que le coût d’un changement de contexte), ou dans le code noyau où il est interdit de dormir, comme les gestionnaires d’interruption.
Mutex
Lorsque le verrou est tenu longtemps, ou que le thread risque de dormir pendant la détention, par exemple pour une E/S ou une allocation mémoire. Exclusion mutuelle ordinaire dans les applications en espace utilisateur.
Combinaison
De nombreuses implémentations de mutex combinent les avantages des deux : sans contention, le verrou est pris sans dormir, uniquement par des instructions atomiques, et en cas d’échec, le thread tourne un peu avant de s’endormir.

Questions fréquentes

Pourquoi utilise-t-on le spinlock alors qu’il gaspille du CPU ?

Parce que, si le verrou est tenu très brièvement, tourner un peu en attendant est plus rapide que les changements de contexte nécessaires pour s’endormir et se réveiller. De plus, là où il est interdit de dormir, comme dans les gestionnaires d’interruption du noyau, seul le spinlock est utilisable.

Pourquoi ne peut-on pas utiliser de mutex dans un gestionnaire d’interruption ?

Un mutex endort le thread en attente lorsqu’il ne peut pas prendre le verrou, mais un gestionnaire d’interruption ne s’exécute pas en tant que thread particulier : il ne peut donc pas s’endormir pour être réveillé plus tard. Pour des données partagées avec un gestionnaire d’interruption, on combine un spinlock et la désactivation des interruptions.

Qu’est-ce qu’un futex ?

C’est un mécanisme de Linux pour implémenter rapidement les mutex, entre autres. Sans contention, il suffit de modifier la variable du verrou en espace utilisateur par des instructions atomiques ; ce n’est qu’en cas de contention, quand il faut attendre, qu’un appel système demande au noyau d’endormir le thread. Comme la plupart des verrous ne sont pas contestés, on économise le coût des appels système.

Que se passe-t-il si l’on utilise un spinlock sur un système à un seul cœur ?

Même en tournant en boucle en espace utilisateur, le thread qui tient le verrou ne peut pas s’exécuter sur ce cœur : le verrou ne se libère pas avant que le thread en attente ait épuisé son quantum de temps. C’est pourquoi, sur un système à un seul cœur, le noyau assure l’exclusion mutuelle en désactivant la préemption ou les interruptions plutôt qu’avec des spinlocks.

Vérifier vos connaissances

Quelle description du verrou à attente active (spinlock) est correcte ?

  1. AVerrou qui endort le thread jusqu’à la libération et le fait réveiller par l’OS
  2. BVerrou qui attend la libération en examinant en boucle la variable de verrou
  3. CMécanisme qui protège les données en faisant tourner un disque
Afficher la réponse et l'explication

Réponse : B. Verrou qui attend la libération en examinant en boucle la variable de verrou

Le spinlock tente de prendre le verrou avec une instruction atomique et, en cas d’échec, réessaie immédiatement, de façon répétée. Sans le coût d’appeler l’OS pour s’endormir puis se réveiller, il est rapide lorsque le verrou n’est détenu que très brièvement. Il est très utilisé dans le noyau de l’OS et pour l’exclusion mutuelle avec les gestionnaires d’interruption. En revanche, il a l’inconvénient de continuer à consommer du CPU pendant l’attente.

Dans quelle situation le spinlock est-il le moins adapté ?

  1. ALorsque le verrou est détenu longtemps et que les threads en attente gaspillent du CPU
  2. BLorsque le verrou n’est détenu que quelques dizaines de nanosecondes
  3. CDans un traitement du noyau de l’OS qui ne peut pas s’endormir
Afficher la réponse et l'explication

Réponse : A. Lorsque le verrou est détenu longtemps et que les threads en attente gaspillent du CPU

Un thread qui attend un spinlock continue d’utiliser le CPU jusqu’à la libération. Si le verrou est détenu longtemps, tout ce temps CPU est gaspillé et gêne l’exécution des autres threads. En particulier, avec un seul cœur, le verrou ne se libère que si le thread détenteur s’exécute ; l’attente dure donc jusqu’à la fin du quantum de temps. Pour les attentes longues, on utilise un mutex qui attend en dormant ; il existe aussi des méthodes qui tournent brièvement avant de s’endormir.

Dans un spinlock, pourquoi attendre la libération par des lectures ordinaires et n’utiliser l’instruction atomique que lorsque le verrou semble libre (test-and-test-and-set), plutôt que de tenter directement la modification par instruction atomique ?

  1. AParce que la lecture ordinaire est plus exacte que l’instruction atomique
  2. BParce qu’une instruction atomique ne peut être utilisée qu’une fois par programme
  3. CParce qu’une simple lecture peut attendre dans son propre cache, ce qui réduit fortement les allers-retours de la ligne de cache entre les cœurs
Afficher la réponse et l'explication

Réponse : C. Parce qu’une simple lecture peut attendre dans son propre cache, ce qui réduit fortement les allers-retours de la ligne de cache entre les cœurs

Une instruction atomique implique une écriture ; si les threads en attente l’exécutent de façon répétée, ils se disputent à chaque fois la ligne en état M et invalident sans cesse les copies des autres cœurs. Avec des lectures ordinaires, chaque cœur se contente de lire sa copie en état S dans son propre cache, sans aucun échange. Ce n’est que lorsque le verrou est libéré et que la valeur change que l’instruction atomique est utilisée pour le prendre.

  1. Quelle comparaison entre mutex (verrou à attente en sommeil) et spinlock est correcte ?
  2. Quelle description de l'inversion de priorité dans un RTOS est correcte ?
  3. Quel est le principe correct de « l'héritage de priorité », utilisé pour prévenir l'inversion de priorité ?

Questions d'entraînement sur Multicœur et synchronisation

Termes clés expliqués

Verrou tournant (spinlock)
Verrou dont l'attente consiste à tester en boucle la variable de verrou jusqu'à ce qu'il se libère. Rapide pour les attentes courtes, mais le CPU reste occupé pendant l'attente.
Mutex
Verrou qui ne peut être détenu que par un seul thread à la fois. En général, un thread qui ne peut l'obtenir est endormi et cède le CPU, puis réveillé par le système d'exploitation à la libération.
Changement de contexte
Bascule du processus ou du thread exécuté par le CPU. L'état (registres, compteur ordinal, etc.) est sauvegardé et restauré ; lors d'un changement de processus, l'espace d'adressage est aussi changé.
Instruction atomique
Instruction qui effectue lecture et écriture mémoire en une seule opération indivisible, sans interruption par un autre cœur : CAS, fetch-and-add, LR/SC, etc.
Inversion de priorité
Phénomène où, pendant qu'une tâche de haute priorité attend une ressource détenue par une tâche de basse priorité, une tâche de priorité intermédiaire préempte cette dernière et continue de s'exécuter, faisant attendre longtemps la tâche prioritaire.

Comparatifs associés