In
nebenläufigem Code hast du rein prinzipiell zwei grundlegende Probleme: Wenn mehrere Threads mit dem selben Objekt arbeiten, muss sichergestellt sein, dass das Objekt sich zu jedem Zeitpunkt in einem gültigen Zustand befindet. Stell dir vor ein Thread schreibt einen neuen Wert in einen Pointer während ein anderer Thread parallel den Wert des Pointers liest. Es könnte nun passieren, dass die ersten 4 Byte des Pointers schon den neuen Wert haben, während die restlichen 4 Byte noch vom alten Wert sind und der Thread somit einen völlig ungültigen Müllpointer liest. Um solche Situationen zu vermeiden, muss jeder Zugriff (lesen
und schreiben) auf nebenläufig verwendete Objekte
atomar erfolgen, d.H. auf eine Art und Weise die Sicherstellt, dass jeder Thread immer nur entweder den neuen oder alten Zustand eines Objektes sieht, aber niemals irgendwas dazwischen. Dies muss sowohl seitens der Hardware als auch seitens des Compilers sichergestellt werden (entsprechende Verwendung entsprechender Maschineninstruktionen).
Das zweite Problem ist die Reihenfolge in der Zustandsänderungen sichtbar werden. Selbst wenn du schon sichergestellt hast, dass nebenläufige Zugriffe auf ein geteiltes Objekt atomar sind, gibt es im Allgemeinen keine Garantie darüber, ab wann welcher Thread welchen konkreten Zustand eines Objektes sieht (durch die Atomizität ist nur garantiert, dass jeder Thread zu jedem Zeitpunkt irgendeinen der gültigen Zustände sieht, den das Objekt irgendwann einmal hatte). Insbesondere gibt es keine Garantie darüber, in welcher Reihenfolge nichtatomare Zugriffe auf anderen Objekten relativ zu atomaren Zugriffen passieren. Auf Hardwareebene gibt es beispielsweise Dinge wie Caches und Store-Buffer, die gerade erst verhindern sollen, dass jeder Zugriff immer bis ganz runter zum langsamen RAM gehen muss bevor weitergemacht werden kann. Eine Folge davon ist aber eben, dass für eine Speicherstelle nicht auf jedem Core zu jedem Zeitpunkt der selbe Wert aufscheint (während ein Core eine Speicherstelle modfiziert hat, kann sich im Cache eines anderen Core noch ein alter Wert für diese Speicherstelle befinden; je nachdem auf welchem Core dein Thread gerade läuft, sieht er also einen anderen Wert für die selbe Speicherstelle).
Eine sinnvolle Kommunikation zwischen Threads ist aber erst möglich, wenn bestimmte Bedingungen bezüglich der Reihenfolge, in der die Modifikation der Zustände von Objekten relativ zueinander passieren, eingehalten werden. Wenn Thread A beispielsweise Daten generiert und über ein atomares Flag einem anderen Thread B signalisieren will, wann die Daten verfügbar sind, ist es wesentlich, für Thread B zu garantieren, dass er niemals den veränderten Wert des Flag sehen kann, bevor er auch die neuen Werte der Daten sehen kann. Memory Fences dienen dazu, solche Bedingungen bezüglich der Reihenfolge, in der bestimmte Modifikationen relativ zueinander sichtbar werden, auszudrücken (der "Zaun" verhindert je nachdem dass der Effekt späterer Zugriffe vor einem bestimmten Punkt bzw. der Effekt früherer Zugriffe nach einem bestimmten Punkt eintritt; er hält also Speicherzugriffe von ihrer Wanderschaft über einen bestimmten Punkt hinaus ab, hence the name).
Wie du dir sicher vorstellen kannst, ist es extrem schwer, auf Ebene von atomaren Operationen und Fences korrekten Code zu schreiben. Daher gibt es auf höherer Ebene Konzepte wie das eines
kritischen Abschnittes, der durch Objekte wie Mutexe bzw. Semaphoren so abgesichert wird, dass zu jedem Zeitpunkt immer nur ein Thread bzw. eine bestimmte Anzahl an Threads den darin befindlichen Code ausführen können. Diese Objekte werden in der Regel vom Betriebssystem zur Verfügung gestellt. Versucht ein Thread einen solchen Abschnitt zu betreten während ein anderer Thread sich darin befindet, wird er vom Betriebssystem angehalten bis der erste Thread den Abschnitt verlassen hat. Das Betriebssystem wird unter der Haube atomare Operationen und Fences verwenden, um solche Threadsynchronisation auf der jeweiligen Hardware korrekt zu implementieren. Man sollte festhalten, dass (entgegen frühreren Behauptungen hier
) derartiges Verhalten im Allgemeinen nicht einfach so selbst gemacht werden kann, sondern rein prinzipiell im Betriebssystem implementiert werden muss, da man so Dinge wie Threads schlafen legen und richtig wieder aufwecken nur im Kernel machen kann (an denjenigen, der jetzt gleich mit Fibers ankommt: let's not go there)...