Ich versuche, dies selbst zu beantworten, nachdem ich verschiedene Online-Ressourcen (z. B. diese und diese ), den C ++ 11-Standard sowie die hier gegebenen Antworten durchgesehen habe.
Die zugehörigen Fragen werden zusammengeführt (z. B. " Warum! Erwartet? " Wird mit "Warum vergleiche_exchange_weak () in eine Schleife setzen? " Zusammengeführt) und die Antworten werden entsprechend angegeben.
Warum muss compare_exchange_weak () in fast allen Anwendungen in einer Schleife sein?
Typisches Muster A.
Sie müssen eine atomare Aktualisierung basierend auf dem Wert in der atomaren Variablen durchführen. Ein Fehler zeigt an, dass die Variable nicht mit unserem gewünschten Wert aktualisiert wurde und wir es erneut versuchen möchten. Beachten Sie, dass es uns egal ist, ob es aufgrund eines gleichzeitigen Schreibvorgangs oder eines falschen Fehlers fehlschlägt. Aber es ist uns wichtig, dass wir diese Änderung vornehmen.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Ein Beispiel aus der Praxis besteht darin, dass mehrere Threads gleichzeitig ein Element zu einer einzeln verknüpften Liste hinzufügen. Jeder Thread lädt zuerst den Kopfzeiger, weist einen neuen Knoten zu und hängt den Kopf an diesen neuen Knoten an. Schließlich wird versucht, den neuen Knoten mit dem Kopf zu tauschen.
Ein weiteres Beispiel ist die Implementierung von Mutex mit std::atomic<bool>
. Allenfalls kann ein Thread den kritischen Abschnitt zu einer Zeit ein, je nachdem , welcher Thread zuerst eingestellt current
auf true
und um die Schleife zu beenden.
Typisches Muster B.
Dies ist eigentlich das Muster, das in Anthonys Buch erwähnt wird. Im Gegensatz zu Muster A möchten Sie, dass die atomare Variable einmal aktualisiert wird, aber es ist Ihnen egal, wer dies tut. Solange es nicht aktualisiert wird, versuchen Sie es erneut. Dies wird normalerweise mit booleschen Variablen verwendet. Sie müssen beispielsweise einen Trigger implementieren, damit eine Zustandsmaschine weitergeht. Welcher Thread den Abzug drückt, ist unabhängig.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Beachten Sie, dass wir dieses Muster im Allgemeinen nicht zum Implementieren eines Mutex verwenden können. Andernfalls können sich mehrere Threads gleichzeitig im kritischen Bereich befinden.
Das heißt, es sollte selten sein, compare_exchange_weak()
außerhalb einer Schleife zu verwenden. Im Gegenteil, es gibt Fälle, in denen die starke Version verwendet wird. Z.B,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
ist hier nicht richtig, denn wenn es aufgrund eines falschen Fehlers zurückkehrt, ist es wahrscheinlich, dass noch niemand den kritischen Abschnitt belegt.
Hungernder Faden?
Ein erwähnenswerter Punkt ist, was passiert, wenn weiterhin falsche Fehler auftreten und der Faden aushungert? Theoretisch könnte es auf Plattformen passieren, wenn compare_exchange_XXX()
es als eine Folge von Anweisungen implementiert wird (z. B. LL / SC). Häufiger Zugriff auf dieselbe Cache-Zeile zwischen LL und SC führt zu kontinuierlichen Störfehlern. Ein realistischeres Beispiel ist eine dumme Planung, bei der alle gleichzeitigen Threads auf folgende Weise verschachtelt werden.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Kann es passieren?
Zum Glück wird es nicht für immer passieren, dank der Anforderungen von C ++ 11:
Implementierungen sollten sicherstellen, dass schwache Vergleichs- und Austauschoperationen nicht konsistent false zurückgeben, es sei denn, das atomare Objekt hat einen anderen als den erwarteten Wert oder es werden gleichzeitig Änderungen am atomaren Objekt vorgenommen.
Warum verwenden wir compare_exchange_weak () und schreiben die Schleife selbst? Wir können einfach compare_exchange_strong () verwenden.
Es hängt davon ab, ob.
Fall 1: Wenn beide innerhalb einer Schleife verwendet werden müssen. C ++ 11 sagt:
Wenn sich ein Vergleich und Austausch in einer Schleife befindet, bietet die schwache Version auf einigen Plattformen eine bessere Leistung.
Auf x86 (zumindest derzeit. Vielleicht wird eines Tages auf ein ähnliches Schema wie LL / SC zurückgegriffen, um die Leistung zu verbessern, wenn mehr Kerne eingeführt werden) sind die schwache und die starke Version im Wesentlichen gleich, da beide auf die einzelne Anweisung hinauslaufen cmpxchg
. Auf einigen anderen Plattformen, auf denen compare_exchange_XXX()
keine atomare Implementierung erfolgt (hier bedeutet dies, dass kein einzelnes Hardware-Grundelement vorhanden ist), kann die schwache Version innerhalb der Schleife den Kampf gewinnen, da die starke Version die falschen Fehler behandeln und es erneut versuchen muss.
Aber,
selten, so können wir es vorziehen , compare_exchange_strong()
über compare_exchange_weak()
selbst in einer Schleife. Wenn beispielsweise viel zu tun ist, wird eine atomare Variable geladen und ein berechneter neuer Wert ausgetauscht (siehe function()
oben). Wenn sich die atomare Variable selbst nicht häufig ändert, müssen wir die kostspielige Berechnung nicht für jeden falschen Fehler wiederholen. Stattdessen können wir hoffen, dass compare_exchange_strong()
solche Fehler "absorbiert" werden, und wir wiederholen die Berechnung nur, wenn sie aufgrund einer tatsächlichen Wertänderung fehlschlägt.
Fall 2: Wenn nur compare_exchange_weak()
innerhalb einer Schleife verwendet werden muss. C ++ 11 sagt auch:
Wenn ein schwacher Vergleich und Austausch eine Schleife erfordern würde und eine starke nicht, ist die starke vorzuziehen.
Dies ist normalerweise der Fall, wenn Sie eine Schleife ausführen, um falsche Fehler in der schwachen Version zu beseitigen. Sie versuchen es erneut, bis der Austausch entweder erfolgreich ist oder aufgrund des gleichzeitigen Schreibens fehlgeschlagen ist.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Bestenfalls erfindet es die Räder neu und funktioniert genauso wie compare_exchange_strong()
. Schlechter? Dieser Ansatz nutzt Maschinen, die einen nicht störenden Vergleich und Austausch von Hardware ermöglichen, nicht vollständig aus .
Wenn Sie für andere Dinge eine Schleife durchführen (siehe z. B. "Typisches Muster A" oben), besteht eine gute Chance, dass diese compare_exchange_strong()
auch in eine Schleife eingefügt wird, die uns zum vorherigen Fall zurückführt.