Während ein Mutex verwendet werden kann, um andere Probleme zu lösen, besteht der Hauptgrund dafür, dass sie sich gegenseitig ausschließen und dadurch eine sogenannte Race-Bedingung lösen. Wenn zwei (oder mehr) Threads oder Prozesse gleichzeitig versuchen, auf dieselbe Variable zuzugreifen, besteht die Möglichkeit einer Race-Bedingung. Betrachten Sie den folgenden Code
//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
i++;
}
Die Interna dieser Funktion sehen so einfach aus. Es ist nur eine Aussage. Ein typisches Pseudo-Assembler-Äquivalent könnte jedoch sein:
load i from memory into a register
add 1 to i
store i back into memory
Da alle äquivalenten Anweisungen in Assemblersprache erforderlich sind, um die Inkrementierungsoperation für i auszuführen, sagen wir, dass das Inkrementieren von i eine nicht atmosphärische Operation ist. Eine atomare Operation kann auf der Hardware mit der Garantie abgeschlossen werden, dass sie nicht unterbrochen wird, sobald die Befehlsausführung begonnen hat. Das Inkrementieren von i besteht aus einer Kette von 3 atomaren Anweisungen. In einem gleichzeitigen System, in dem mehrere Threads die Funktion aufrufen, treten Probleme auf, wenn ein Thread zur falschen Zeit liest oder schreibt. Stellen Sie sich vor, wir haben zwei Threads, die gleichzeitig ausgeführt werden, und einer ruft die Funktion unmittelbar nach dem anderen auf. Nehmen wir auch an, wir haben i auf 0 initialisiert. Nehmen wir außerdem an, dass wir viele Register haben und dass die beiden Threads völlig unterschiedliche Register verwenden, sodass es nicht zu Kollisionen kommt. Der tatsächliche Zeitpunkt dieser Ereignisse kann sein:
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
Was passiert ist, ist, dass wir zwei Threads haben, die i gleichzeitig inkrementieren. Unsere Funktion wird zweimal aufgerufen, aber das Ergebnis stimmt nicht mit dieser Tatsache überein. Es sieht so aus, als ob die Funktion nur einmal aufgerufen wurde. Dies liegt daran, dass die Atomizität auf Maschinenebene "gebrochen" ist, was bedeutet, dass sich Threads gegenseitig unterbrechen oder zu falschen Zeiten zusammenarbeiten können.
Wir brauchen einen Mechanismus, um dies zu lösen. Wir müssen den obigen Anweisungen eine Bestellung auferlegen. Ein üblicher Mechanismus besteht darin, alle Threads außer einem zu blockieren. Pthread Mutex verwendet diesen Mechanismus.
Jeder Thread, der einige Codezeilen ausführen muss, die möglicherweise gemeinsam genutzte Werte von anderen Threads gleichzeitig unsicher ändern (über das Telefon mit seiner Frau sprechen), sollte zuerst eine Sperre für einen Mutex erhalten. Auf diese Weise muss jeder Thread, der Zugriff auf die gemeinsam genutzten Daten benötigt, die Mutex-Sperre durchlaufen. Nur dann kann ein Thread den Code ausführen. Dieser Codeabschnitt wird als kritischer Abschnitt bezeichnet.
Sobald der Thread den kritischen Abschnitt ausgeführt hat, sollte er die Sperre für den Mutex aufheben, damit ein anderer Thread eine Sperre für den Mutex erhalten kann.
Das Konzept eines Mutex scheint etwas seltsam, wenn man Menschen betrachtet, die exklusiven Zugang zu realen, physischen Objekten suchen, aber wenn wir programmieren, müssen wir absichtlich sein. Gleichzeitige Threads und Prozesse haben nicht die soziale und kulturelle Erziehung, die wir haben, daher müssen wir sie zwingen, Daten gut auszutauschen.
Wie funktioniert ein Mutex technisch gesehen? Leidet es nicht unter den gleichen Rennbedingungen, die wir zuvor erwähnt haben? Ist pthread_mutex_lock () nicht etwas komplexer als ein einfaches Inkrementieren einer Variablen?
Technisch gesehen benötigen wir Hardware-Unterstützung, um uns zu helfen. Die Hardware-Designer geben uns Maschinenanweisungen, die mehr als eine Sache tun, aber garantiert atomar sind. Ein klassisches Beispiel für eine solche Anweisung ist das Test-and-Set (TAS). Wenn wir versuchen, eine Sperre für eine Ressource zu erlangen, prüfen wir möglicherweise mithilfe des TAS, ob ein Wert im Speicher 0 ist. Wenn dies der Fall ist, ist dies unser Signal dafür, dass die Ressource verwendet wird und wir nichts (oder genauer) tun Wir warten durch einen Mechanismus. Ein Pthreads-Mutex versetzt uns in eine spezielle Warteschlange im Betriebssystem und benachrichtigt uns, wenn die Ressource verfügbar wird. Bei dümmeren Systemen müssen wir möglicherweise eine enge Schleife durchführen und den Zustand immer wieder testen. . Wenn der Wert im Speicher nicht 0 ist, setzt der TAS den Speicherort auf einen anderen Wert als 0, ohne andere Anweisungen zu verwenden. Es' Es ist so, als würde man zwei Montageanweisungen zu einer kombinieren, um Atomizität zu erhalten. Daher kann das Testen und Ändern des Werts (falls eine Änderung angemessen ist) nicht unterbrochen werden, sobald er begonnen hat. Wir können Mutexe auf einer solchen Anweisung aufbauen.
Hinweis: Einige Abschnitte ähneln möglicherweise einer früheren Antwort. Ich nahm seine Einladung zur Bearbeitung an, er bevorzugte die ursprüngliche Art und Weise, also behalte ich das, was ich hatte, was mit ein wenig seiner Redewendung durchsetzt ist.