Bien qu'un mutex puisse être utilisé pour résoudre d'autres problèmes, la principale raison pour laquelle ils existent est de fournir une exclusion mutuelle et de résoudre ainsi ce que l'on appelle une condition de race. Lorsque deux (ou plus) threads ou processus tentent d'accéder à la même variable simultanément, nous avons un potentiel pour une condition de concurrence. Considérez le code suivant
//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
i++;
}
Les éléments internes de cette fonction semblent si simples. Ce n'est qu'une seule déclaration. Cependant, un équivalent en langage pseudo-assembleur typique pourrait être:
load i from memory into a register
add 1 to i
store i back into memory
Étant donné que les instructions équivalentes en langage assembleur sont toutes nécessaires pour effectuer l'opération d'incrémentation sur i, nous disons que l'incrémentation de i est une opération non atmosphérique. Une opération atomique est une opération qui peut être effectuée sur le matériel avec une garantie de ne pas être interrompue une fois que l'exécution de l'instruction a commencé. L'incrémentation de i consiste en une chaîne de 3 instructions atomiques. Dans un système simultané où plusieurs threads appellent la fonction, des problèmes surviennent lorsqu'un thread lit ou écrit au mauvais moment. Imaginez que nous ayons deux threads exécutés simultanément et que l'un appelle la fonction immédiatement après l'autre. Disons aussi que nous avons initialisé i à 0. Supposons également que nous avons beaucoup de registres et que les deux threads utilisent des registres complètement différents, il n'y aura donc pas de collisions. Le moment réel de ces événements peut être:
thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1
Ce qui s'est passé, c'est que nous avons deux threads incrémentant i simultanément, notre fonction est appelée deux fois, mais le résultat est incompatible avec ce fait. Il semble que la fonction n'a été appelée qu'une seule fois. C'est parce que l'atomicité est "cassée" au niveau de la machine, ce qui signifie que les threads peuvent s'interrompre ou travailler ensemble aux mauvais moments.
Nous avons besoin d'un mécanisme pour résoudre ce problème. Nous devons imposer un ordre aux instructions ci-dessus. Un mécanisme courant consiste à bloquer tous les threads sauf un. Pthread mutex utilise ce mécanisme.
Tout thread qui doit exécuter des lignes de code qui peuvent modifier de manière non sécurisée les valeurs partagées par d'autres threads en même temps (en utilisant le téléphone pour parler à sa femme), doit d'abord être obligé d'acquérir un verrou sur un mutex. De cette façon, tout thread qui nécessite l'accès aux données partagées doit passer par le verrou mutex. Ce n'est qu'alors qu'un thread pourra exécuter le code. Cette section de code est appelée une section critique.
Une fois que le thread a exécuté la section critique, il doit libérer le verrou sur le mutex afin qu'un autre thread puisse acquérir un verrou sur le mutex.
Le concept d'avoir un mutex semble un peu étrange lorsque l'on considère les humains cherchant un accès exclusif à des objets réels et physiques, mais lors de la programmation, nous devons être intentionnels. Les fils et processus simultanés n'ont pas l'éducation sociale et culturelle que nous faisons, nous devons donc les forcer à partager les données correctement.
Alors techniquement, comment fonctionne un mutex? Ne souffre-t-il pas des mêmes conditions de course que nous avons évoquées plus tôt? Pthread_mutex_lock () n'est-il pas un peu plus complexe qu'un simple incrément d'une variable?
Techniquement parlant, nous avons besoin d'un support matériel pour nous aider. Les concepteurs de matériel nous donnent des instructions machine qui font plus d'une chose mais sont garanties atomiques. Un exemple classique d'une telle instruction est le test-and-set (TAS). Lorsque vous essayez d'acquérir un verrou sur une ressource, nous pouvons utiliser le TAS pour vérifier si une valeur en mémoire est 0. Si c'est le cas, ce serait notre signal que la ressource est en cours d'utilisation et que nous ne faisons rien (ou plus précisément , nous attendons par un mécanisme. Un mutex pthreads nous mettra dans une file d'attente spéciale dans le système d'exploitation et nous avertira lorsque la ressource sera disponible. Les systèmes plus stupides peuvent nous obliger à faire une boucle de rotation serrée, en testant la condition encore et encore) . Si la valeur en mémoire n'est pas 0, le TAS définit l'emplacement sur autre chose que 0 sans utiliser d'autres instructions. Il' C'est comme combiner deux instructions d'assemblage en 1 pour nous donner l'atomicité. Ainsi, le test et la modification de la valeur (si la modification est appropriée) ne peuvent pas être interrompus une fois qu'ils ont commencé. Nous pouvons construire des mutex en plus d'une telle instruction.
Remarque: certaines sections peuvent ressembler à une réponse précédente. J'ai accepté son invitation à éditer, il a préféré la manière originale, donc je garde ce que j'avais qui est imprégné d'un peu de son verbiage.