Ich verstehe, dass dies std::atomic<>
ein atomares Objekt ist. Aber inwieweit atomar? Nach meinem Verständnis kann eine Operation atomar sein. Was genau bedeutet es, ein Objekt atomar zu machen? Zum Beispiel, wenn zwei Threads gleichzeitig den folgenden Code ausführen:
a = a + 12;
Ist dann die gesamte Operation (sagen wir add_twelve_to(int)
) atomar? Oder werden Änderungen an der Variablen atomar (so operator=()
) vorgenommen?
c++
multithreading
c++11
atomic
Neugieriger
quelle
quelle
a.fetch_add(12)
wenn Sie ein atomares RMW wollen.std::atomic
die Standardbibliothek entscheiden, was zur Erzielung der Atomizität erforderlich ist.std::atomic<T>
ist ein Typ, der atomare Operationen ermöglicht. Es macht dein Leben nicht auf magische Weise besser, du musst immer noch wissen, was du damit machen willst. Es ist für einen sehr spezifischen Anwendungsfall gedacht, und die Verwendung von atomaren Operationen (am Objekt) ist im Allgemeinen sehr subtil und muss aus einer nicht lokalen Perspektive betrachtet werden. Wenn Sie das nicht bereits wissen und wissen, warum Sie atomare Operationen wünschen, ist der Typ für Sie wahrscheinlich nicht von großem Nutzen.Antworten:
Jede Instanziierung und vollständige Spezialisierung von std :: atomic <> stellt einen Typ dar, mit dem verschiedene Threads gleichzeitig arbeiten können (ihre Instanzen), ohne undefiniertes Verhalten auszulösen:
std::atomic<>
Wraps-Operationen, die in Pre-C ++ 11-mal mit (zum Beispiel) ineinandergreifenden Funktionen mit MSVC oder atomaren Bultinen im Fall von GCC ausgeführt werden mussten.Außerdem
std::atomic<>
gibt Ihnen mehr Kontrolle durch verschiedene ermöglicht Speicheraufträge , die Synchronisation und Bestellbeschränkungen festlegen. Wenn Sie mehr über C ++ 11 Atomics und das Speichermodell erfahren möchten, können diese Links hilfreich sein:Beachten Sie, dass Sie für typische Anwendungsfälle wahrscheinlich überladene arithmetische Operatoren oder einen anderen Satz davon verwenden würden :
Da Sie mit der Operatorsyntax die Speicherreihenfolge nicht angeben können, werden diese Operationen mit ausgeführt
std::memory_order_seq_cst
, da dies die Standardreihenfolge für alle atomaren Operationen in C ++ 11 ist. Sie garantiert die sequentielle Konsistenz (globale Gesamtreihenfolge) zwischen allen atomaren Operationen.In einigen Fällen ist dies jedoch möglicherweise nicht erforderlich (und nichts ist kostenlos). Daher möchten Sie möglicherweise eine explizitere Form verwenden:
Nun Ihr Beispiel:
wird nicht zu einer einzelnen atomaren Operation ausgewertet: Es wird
a.load()
(was selbst atomar ist), dann Addition zwischen diesem Wert und12
unda.store()
(auch atomar) des Endergebnisses. Wie ich bereits erwähnt habe,std::memory_order_seq_cst
wird hier verwendet.Wenn Sie jedoch schreiben
a += 12
, handelt es sich um eine atomare Operation (wie bereits erwähnt), die in etwa der entsprichta.fetch_add(12, std::memory_order_seq_cst)
.Wie für Ihren Kommentar:
Ihre Aussage gilt nur für Architekturen, die eine solche Atomaritätsgarantie für Geschäfte und / oder Lasten bieten. Es gibt Architekturen, die dies nicht tun. Außerdem ist es normalerweise erforderlich, dass Operationen an wort- / dword-ausgerichteten Adressen ausgeführt werden müssen, um atomar zu sein. Dies
std::atomic<>
ist etwas, das auf jeder Plattform ohne zusätzliche Anforderungen garantiert atomar ist . Darüber hinaus können Sie Code wie folgt schreiben:Beachten Sie, dass die Assertionsbedingung immer wahr ist (und daher niemals ausgelöst wird), sodass Sie immer sicher sein können, dass die Daten nach dem
while
Beenden der Schleife bereit sind . Das ist, weil:store()
to the flag wird ausgeführt, nachdemsharedData
gesetzt wurde (wir gehen davon aus, dassgenerateData()
immer etwas Nützliches zurückgegeben wird, insbesondere nie zurückgegeben wirdNULL
) und verwendet diestd::memory_order_release
Reihenfolge:sharedData
wird nach demwhile
Beenden der Schleife verwendet und gibt daher nach demload()
from-Flag einen Wert ungleich Null zurück.load()
verwendetstd::memory_order_acquire
Reihenfolge:Dies gibt Ihnen eine genaue Kontrolle über die Synchronisation und ermöglicht es Ihnen, explizit anzugeben, wie sich Ihr Code möglicherweise / möglicherweise nicht / wird / nicht verhält. Dies wäre nicht möglich, wenn nur die Atomizität selbst garantiert wäre. Besonders wenn es um sehr interessante Synchronisationsmodelle wie die Release-Consum-Bestellung geht .
quelle
int
s haben?std::atomic
(std::memory_order
) vorgesehenen Bestellbeschränkungen dienen genau dem Zweck, die zulässigen Nachbestellungen zu begrenzen.Das ist eine Frage der Perspektive ... Sie können es nicht auf beliebige Objekte anwenden und deren Operationen atomar werden lassen, aber die bereitgestellten Spezialisierungen für (die meisten) integralen Typen und Zeiger können verwendet werden.
std::atomic<>
vereinfacht dies nicht (verwenden Sie Vorlagenausdrücke, um dies zu vereinfachen) zu einer einzelnen atomaren Operation, stattdessen führt dasoperator T() const volatile noexcept
Mitglied eine atomare Operationload()
ausa
, dann werden zwölf hinzugefügt undoperator=(T t) noexcept
astore(t)
.quelle
int
wird nicht sichergestellt, dass die Änderung von anderen Threads aus sichtbar ist, und durch das Lesen wird auch nicht sichergestellt, dass die Änderungen anderer Threads angezeigt werden. Einige Dinge wiemy_int += 3
werden nicht garantiert atomar ausgeführt, es sei denn, Sie verwendenstd::atomic<>
sie ein Abruf, dann Hinzufügen, dann Speichern der Sequenz, wobei ein anderer Thread, der versucht, denselben Wert zu aktualisieren, möglicherweise nach dem Abrufen und vor dem Speichern eingeht und die Aktualisierung Ihres Threads blockiert.std::atomic
existiert, weil viele ISAs direkte Hardwareunterstützung dafür habenWas der C ++ - Standard sagt,
std::atomic
wurde in anderen Antworten analysiert.Nun wollen wir sehen, was
std::atomic
kompiliert wird, um einen anderen Einblick zu erhalten.Die wichtigste Erkenntnis aus diesem Experiment ist, dass moderne CPUs direkte Unterstützung für atomare Ganzzahloperationen, beispielsweise das LOCK-Präfix in x86, haben und im
std::atomic
Grunde genommen als tragbare Schnittstelle zu diesen Anweisungen existieren: Was bedeutet der Befehl "lock" in der x86-Assembly? In aarch64 würde LDADD verwendet.Diese Unterstützung ermöglicht eine schnelleren Alternativen zu allgemeineren Methoden wie
std::mutex
, die komplexen Multi-Instruktions Abschnitte Atom machen kann, auf Kosten des Seins langsamer als ,std::atomic
weilstd::mutex
es machtfutex
Systemaufrufe in Linux, die Art und Weise langsamer als die Userland - Anweisungen ist emittieren durchstd::atomic
, siehe auch: Erstellt std :: mutex einen Zaun?Betrachten wir das folgende Multithread-Programm, das eine globale Variable über mehrere Threads mit unterschiedlichen Synchronisationsmechanismen inkrementiert, je nachdem, welche Präprozessordefinition verwendet wird.
main.cpp
GitHub stromaufwärts .
Kompilieren, ausführen und zerlegen:
Sehr wahrscheinlich "falsche" Ausgabe der Rennbedingungen für
main_fail.out
:und deterministische "richtige" Ausgabe der anderen:
Demontage von
main_fail.out
:Demontage von
main_std_atomic.out
:Demontage von
main_lock.out
:Schlussfolgerungen:
Die nichtatomare Version speichert die globale Version in einem Register und erhöht das Register.
Daher werden am Ende sehr wahrscheinlich vier Schreibvorgänge mit demselben "falschen" Wert von auf global zurückgeführt
100000
.std::atomic
kompiliert zulock addq
. Das LOCK-Präfix bewirkt, dass der folgendeinc
Speicher atomar abgerufen, geändert und aktualisiert wird.Unser explizites Inline-Assembly-LOCK-Präfix wird fast genauso kompiliert wie
std::atomic
, außer dass unserinc
anstelle verwendet wirdadd
. Ich bin mir nicht sicher, warum GCC sich entschieden hatadd
, wenn man bedenkt, dass unser INC eine um 1 Byte kleinere Decodierung generiert hat.ARMv8 kann in neueren CPUs entweder LDAXR + STLXR oder LDADD verwenden: Wie starte ich Threads in normalem C?
Getestet in Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
quelle