Angenommen, diese Threads werden in einer Single-Core-CPU ausgeführt. Als CPU nur einen Befehl in einem Zyklus ausführen. Das heißt, obwohl sie die CPU-Ressource gemeinsam nutzen. aber der Computer stellt sicher, dass eine einmalige Anweisung. Ist die Sperre also für das Multi-Threading nicht erforderlich?
multithreading
Python
quelle
quelle
Antworten:
Dies lässt sich am besten anhand eines Beispiels veranschaulichen.
Angenommen, wir haben eine einfache Aufgabe, die wir mehrmals parallel ausführen möchten, und wir möchten global verfolgen, wie oft die Aufgabe ausgeführt wurde, z. B. um Zugriffe auf eine Webseite zu zählen.
Wenn jeder Thread den Punkt erreicht, an dem er die Anzahl erhöht, sieht seine Ausführung folgendermaßen aus:
Denken Sie daran, dass jeder Thread zu jedem Zeitpunkt in diesem Prozess angehalten werden kann. Wenn also Thread A Schritt 1 ausführt und dann angehalten wird, gefolgt von Thread B, der alle drei Schritte ausführt, werden seine Register bei Wiederaufnahme von Thread A die falsche Anzahl von Treffern aufweisen: seine Register werden wiederhergestellt, und die alte Anzahl wird glücklich erhöht von Treffern, und speichern Sie diese erhöhte Zahl.
Darüber hinaus könnte während der Zeit, in der Thread A angehalten wurde, eine beliebige Anzahl anderer Threads ausgeführt worden sein, sodass die Anzahl, die Thread A am Ende schreibt, möglicherweise deutlich unter der korrekten Anzahl liegt.
Aus diesem Grund muss sichergestellt werden, dass ein Thread, der Schritt 1 ausführt, Schritt 3 ausführen muss, bevor ein anderer Thread Schritt 1 ausführen darf. Dies kann von allen Threads durchgeführt werden, die darauf warten, eine einzelne Sperre zu erhalten, bevor sie mit diesem Prozess beginnen , und Freigabe der Sperre erst nach Abschluss des Vorgangs, damit dieser "kritische Abschnitt" des Codes nicht falsch verschachtelt werden kann, was zu einer falschen Zählung führt.
Was aber, wenn die Operation atomar wäre?
Ja, im Land der magischen Einhörner und Regenbogen, in dem die Inkrementierungsoperation atomar ist, wäre für das obige Beispiel keine Verriegelung erforderlich.
Es ist jedoch wichtig zu wissen, dass wir nur sehr wenig Zeit in der Welt der magischen Einhörner und Regenbögen verbringen. In fast jeder Programmiersprache ist die Inkrementierungsoperation in die obigen drei Schritte unterteilt. Dies liegt daran, dass selbst wenn der Prozessor eine atomare Inkrementoperation unterstützt, diese Operation erheblich teurer ist: Sie muss aus dem Speicher lesen, die Nummer ändern und zurück in den Speicher schreiben ... und normalerweise ist die atomare Inkrementoperation eine Operation, die kann fehlschlagen, was bedeutet, dass die einfache Sequenz oben durch eine Schleife ersetzt werden muss (wie wir unten sehen werden).
Da selbst in Multithread-Code viele Variablen für einen einzelnen Thread lokal gehalten werden, sind Programme viel effizienter, wenn sie davon ausgehen, dass jede Variable für einen einzelnen Thread lokal ist, und die Programmierer den gemeinsamen Status zwischen Threads schützen. Insbesondere angesichts der Tatsache, dass Atomoperationen normalerweise nicht ausreichen, um Threading-Probleme zu lösen, wie wir später sehen werden.
Flüchtige Variablen
Wenn wir Sperren für dieses bestimmte Problem vermeiden möchten, müssen wir zuerst erkennen, dass die in unserem ersten Beispiel dargestellten Schritte nicht wirklich das sind, was in modern kompiliertem Code geschieht. Da Compiler davon ausgehen, dass nur ein Thread die Variable ändert, behält jeder Thread seine eigene zwischengespeicherte Kopie der Variablen, bis das Prozessorregister für etwas anderes benötigt wird. Solange die zwischengespeicherte Kopie vorhanden ist, muss sie nicht in den Speicher zurückgeschrieben und erneut gelesen werden (was teuer wäre). Sie schreiben die Variable auch nicht zurück in den Speicher, solange sie in einem Register gespeichert ist.
Wir können zu der Situation zurückkehren, die wir im ersten Beispiel angegeben haben (mit denselben Threading-Problemen, die wir oben identifiziert haben), indem wir die Variable als flüchtig markieren , was dem Compiler mitteilt, dass diese Variable von anderen geändert wird und daher gelesen werden muss oder in den Speicher geschrieben, wenn darauf zugegriffen oder geändert wird.
Eine Variable, die als flüchtig markiert ist, führt uns also nicht in das Land der atomaren Inkrementierungsoperationen, sondern bringt uns nur so nahe, wie wir es bereits dachten.
Das Inkrement atomar machen
Sobald wir eine flüchtige Variable verwenden, können wir unsere Inkrementierungsoperation atomar machen, indem wir eine bedingte Set-Operation auf niedriger Ebene verwenden, die von den meisten modernen CPUs unterstützt wird (häufig als Vergleichen und Setzen oder Vergleichen und Tauschen bezeichnet ). Dieser Ansatz wird beispielsweise in der AtomicInteger- Klasse von Java verwendet :
Die obige Schleife führt die folgenden Schritte wiederholt aus, bis Schritt 3 erfolgreich ist:
Wenn Schritt 3 fehlschlägt (weil der Wert nach Schritt 1 von einem anderen Thread geändert wurde), liest er die Variable erneut direkt aus dem Hauptspeicher und versucht es erneut.
In diesem Fall ist das Vergleichen und Austauschen zwar teuer, aber etwas besser als das Sperren. Wenn ein Thread nach Schritt 1 angehalten wird, müssen andere Threads, die Schritt 1 erreichen, nicht blockieren und auf den ersten Thread warten kann kostspielige Kontextumschaltung verhindern. Wenn der erste Thread fortgesetzt wird, schlägt der erste Versuch, die Variable zu schreiben, fehl, er kann jedoch fortfahren, indem er die Variable erneut liest. Dies ist wiederum wahrscheinlich kostengünstiger als der Kontextwechsel, der beim Sperren erforderlich gewesen wäre.
So können wir durch Vergleichen und Tauschen in das Land der atomaren Inkremente (oder anderer Operationen an einer einzelnen Variablen) gelangen, ohne tatsächliche Sperren zu verwenden.
Wann ist das Sperren also unbedingt erforderlich?
Wenn Sie mehr als eine Variable in einer atomaren Operation ändern müssen, ist eine Sperrung erforderlich, für die Sie keine spezielle Prozessoranweisung finden.
Solange Sie an einer einzelnen Variablen arbeiten und auf die fehlgeschlagene Arbeit vorbereitet sind und die Variable lesen und erneut beginnen müssen, ist Compare-and-Swap jedoch ausreichend.
Angenommen, jeder Thread addiert zuerst 2 zu einer Variablen X und multipliziert dann X mit zwei.
Wenn X anfänglich eins ist und zwei Threads ausgeführt werden, erwarten wir, dass das Ergebnis (((1 + 2) * 2) + 2) * 2 = 16 ist.
Wenn jedoch die Threads verschachteln, können wir, auch wenn alle Operationen atomar sind, stattdessen beide Additionen zuerst auftreten lassen und die Multiplikationen folgen, was zu (1 + 2 + 2) * 2 * 2 = 20 führt.
Dies geschieht, weil Multiplikation und Addition keine kommutativen Operationen sind.
Also, die Operationen selbst sind nicht atomar genug, wir müssen die Kombination von Operationen atomar machen.
Wir können dies entweder durch Sperren zur Serialisierung des Prozesses tun, oder wir können eine lokale Variable verwenden, um den Wert von X zu Beginn unserer Berechnung zu speichern, eine zweite lokale Variable für die Zwischenschritte und dann Compare-and-Swap to verwenden Legen Sie einen neuen Wert nur dann fest, wenn der aktuelle Wert von X mit dem ursprünglichen Wert von X übereinstimmt. Wenn dies fehlschlägt, müssen Sie X erneut lesen und die Berechnungen erneut durchführen.
Es gibt mehrere Kompromisse: Je länger die Berechnungen werden, desto wahrscheinlicher wird es, dass der laufende Thread angehalten wird und der Wert von einem anderen Thread geändert wird, bevor wir fortfahren, was bedeutet, dass Fehler viel wahrscheinlicher werden und zu einer Verschwendung führen Prozessorzeit. Im Extremfall einer großen Anzahl von Threads mit sehr langen Berechnungen können 100 Threads die Variable lesen und an Berechnungen teilnehmen. In diesem Fall kann der neue Wert nur vom ersten bis zum Ende geschrieben werden, die anderen 99 Threads bleiben bestehen Vervollständigen Sie ihre Berechnungen, aber stellen Sie nach Abschluss fest, dass sie den Wert nicht aktualisieren können. An diesem Punkt wird jeder den Wert lesen und die Berechnung von vorne beginnen. Wahrscheinlich werden die verbleibenden 99 Threads das gleiche Problem wiederholen und viel Prozessorzeit verschwenden.
Eine vollständige Serialisierung des kritischen Abschnitts über Sperren wäre in dieser Situation viel besser: 99 Threads würden angehalten, wenn sie die Sperre nicht bekämen, und wir würden jeden Thread in der Reihenfolge ihrer Ankunft am Sperrpunkt ausführen.
Wenn die Serialisierung nicht kritisch ist (wie in unserem inkrementellen Fall) und die Berechnungen, die verloren gehen würden, wenn die Aktualisierung der Nummer fehlschlägt, minimal sind, kann die Verwendung der Compare-and-Swap-Operation aufgrund dieser Operation einen erheblichen Vorteil bringen ist billiger als das Schließen.
quelle
Betrachten Sie dieses Zitat:
Sie sehen, auch wenn 1 Befehl zu einem bestimmten Zeitpunkt auf einer CPU ausgeführt wird, umfassen Computerprogramme viel mehr als nur atomare Montageanweisungen. Wenn Sie beispielsweise in die Konsole (oder in eine Datei) schreiben, müssen Sie die Sperre aktivieren, um sicherzustellen, dass die Konsole wie gewünscht funktioniert.
quelle
Es scheint, dass viele Antworten versucht haben, das Sperren zu erklären, aber ich denke, dass OP eine Erklärung dafür benötigt, was Multitasking tatsächlich ist.
Wenn auf einem System mehr als ein Thread ausgeführt wird, auch wenn nur eine CPU vorhanden ist, gibt es zwei Hauptmethoden, die bestimmen, wie diese Threads geplant werden (dh in Ihre Single-Core-CPU eingefügt werden):
quelle
Das Problem liegt nicht bei den einzelnen Operationen, sondern bei den größeren Aufgaben, die die Operationen ausführen.
Viele Algorithmen werden unter der Annahme geschrieben, dass sie die volle Kontrolle über den Zustand haben, in dem sie arbeiten. Bei einem verschachtelten, geordneten Ausführungsmodell wie dem von Ihnen beschriebenen können die Operationen willkürlich miteinander verschachtelt werden, und wenn sie den Status gemeinsam haben, besteht das Risiko, dass der Status inkonsistent ist.
Sie können es mit Funktionen vergleichen, die eine Invariante vorübergehend unterbrechen, um das zu tun, was sie tun. Solange der Zwischenstaat von außen nicht einsehbar ist, können sie tun, was sie wollen, um ihre Aufgabe zu erfüllen.
Wenn Sie gleichzeitig Code schreiben, müssen Sie sicherstellen, dass der Konfliktstatus als unsicher eingestuft wird, es sei denn, Sie haben exklusiven Zugriff darauf. Der übliche Weg, um einen exklusiven Zugriff zu erzielen, ist die Synchronisation auf einem Synchronisationsprimitiv, wie das Halten einer Sperre.
Eine andere Tendenz, zu der Synchronisationsprimitive auf einigen Plattformen führen, besteht darin, dass sie Speicherbarrieren emittieren, die die Konsistenz des Arbeitsspeichers zwischen den CPUs sicherstellen.
quelle
Mit Ausnahme der Einstellung 'bool' gibt es keine Garantie (zumindest in c), dass das Lesen oder Schreiben einer Variablen nur eine Anweisung erfordert - oder vielmehr nicht während des Lesens / Schreibens unterbrochen werden kann
quelle
bool
diese Eigenschaft haben? Und reden Sie über das Laden aus dem Speicher, das Ändern und Zurückschieben in den Speicher, oder reden Sie auf Registerebene? Alle Lese- / Schreibvorgänge in die Register werden nicht unterbrochen, aber das Laden von Mem und das Speichern von Mem erfolgen nicht (da dies allein 2 Anweisungen sind, muss mindestens 1 weitere Anweisung ausgeführt werden, um den Wert zu ändern).the standard says that only 'bool' needs to be safe against a context switch in the middle of a read/write of a single variable
sollte wirklich zur Antwort hinzugefügt werden.Geteilte Erinnerung.
Es ist die Definition von ... Threads : eine Reihe von gleichzeitigen Prozessen mit gemeinsamem Speicher.
Wenn kein gemeinsamer Speicher vorhanden ist, werden sie normalerweise als Old-School-UNIX- Prozesse bezeichnet.
Sie benötigen jedoch gelegentlich eine Sperre, wenn sie auf eine freigegebene Datei zugreifen.
(Shared Memory in UNIX-ähnlichen Kerneln wurde in der Tat normalerweise mit einem gefälschten Dateideskriptor implementiert, der die Shared Memory-Adresse darstellt.)
quelle
Eine CPU führt jeweils einen Befehl aus, aber was ist, wenn Sie zwei oder mehr CPUs haben?
Sie haben Recht, dass Sperren nicht benötigt werden, wenn Sie das Programm so schreiben können, dass es atomare Anweisungen ausnutzt: Anweisungen, deren Ausführung auf dem angegebenen Prozessor nicht unterbrechbar und frei von Störungen durch andere Prozessoren ist.
Sperren sind erforderlich, wenn mehrere Befehle vor Interferenzen geschützt werden müssen und es keinen äquivalenten atomaren Befehl gibt.
Das Einfügen eines Knotens in eine doppelt verknüpfte Liste erfordert beispielsweise die Aktualisierung mehrerer Speicherorte. Vor dem Einfügen und nach dem Einfügen behalten bestimmte Invarianten die Struktur der Liste bei. Während des Einfügens werden diese Invarianten jedoch vorübergehend unterbrochen: Die Liste befindet sich im Status "In Bearbeitung".
Wenn ein anderer Thread die Liste durchläuft, während die Invarianten oder auch versucht, sie zu ändern, während sie sich in einem solchen Zustand befinden, wird die Datenstruktur möglicherweise beschädigt und das Verhalten ist unvorhersehbar: Möglicherweise stürzt die Software ab oder es werden falsche Ergebnisse angezeigt. Es ist daher notwendig, dass Threads sich irgendwie einig sind, dass sie sich nicht im Weg stehen, wenn die Liste aktualisiert wird.
Entsprechend gestaltete Listen können mit atomaren Anweisungen manipuliert werden, so dass keine Sperren erforderlich sind. Algorithmen hierfür heißen "lock free". Beachten Sie jedoch, dass atomare Anweisungen tatsächlich eine Form der Verriegelung sind. Sie sind speziell in Hardware implementiert und arbeiten über die Kommunikation zwischen Prozessoren. Sie sind teurer als ähnliche Anweisungen, die nicht atomar sind.
Auf Multiprozessoren, denen der Luxus atomarer Anweisungen fehlt, müssen Grundelemente für den gegenseitigen Ausschluss aus einfachen Speicherzugriffen und Abfrageschleifen aufgebaut werden. An solchen Problemen haben Edsger Dijkstra und Leslie Lamport gearbeitet.
quelle