Wenn zwei Threads auf eine globale Variable zugreifen, wird in vielen Tutorials angegeben, dass die Variable flüchtig ist, um zu verhindern, dass der Compiler die Variable in einem Register zwischenspeichert und sie daher nicht korrekt aktualisiert wird. Zwei Threads, die beide auf eine gemeinsam genutzte Variable zugreifen, erfordern Schutz über einen Mutex, nicht wahr? In diesem Fall befindet sich der Code zwischen dem Sperren des Threads und dem Freigeben des Mutex in einem kritischen Abschnitt, in dem nur dieser eine Thread auf die Variable zugreifen kann. In diesem Fall muss die Variable nicht flüchtig sein.
Was ist also die Verwendung / der Zweck von flüchtig in einem Multithread-Programm?
c++
multithreading
concurrency
atomic
volatile
David Preston
quelle
quelle
Antworten:
Kurze und schnelle Antwort :
volatile
Ist (fast) nutzlos für plattformunabhängige Multithread-Anwendungsprogrammierung. Es bietet keine Synchronisation, erstellt keine Speicherzäune und stellt auch nicht die Reihenfolge der Ausführung von Operationen sicher. Operationen werden dadurch nicht atomar. Es macht Ihren Code nicht magisch threadsicher.volatile
ist möglicherweise die am häufigsten missverstandene Funktion in C ++. Sehen Sie dies , dies und dies für weitere Informationen übervolatile
Auf der anderen Seite
volatile
hat eine Verwendung, die möglicherweise nicht so offensichtlich ist. Es kann ähnlich wieconst
der Compiler verwendet werden, um Ihnen zu zeigen, wo Sie möglicherweise einen Fehler beim Zugriff auf eine gemeinsam genutzte Ressource auf nicht geschützte Weise machen. Diese Verwendung wird von Alexandrescu in diesem Artikel erörtert . Dies verwendet jedoch im Wesentlichen das System vom Typ C ++ auf eine Weise, die häufig als Erfindung angesehen wird und undefiniertes Verhalten hervorrufen kann.volatile
wurde speziell für die Schnittstelle mit speicherabgebildeter Hardware, Signalhandlern und der Anweisung setjmp machine code verwendet. Dies istvolatile
direkt auf die Programmierung auf Systemebene anwendbar und nicht auf die normale Programmierung auf Anwendungsebene.Der 2003 C ++ Standard sagt nicht, dass
volatile
irgendeine Art von Acquire- oder Release-Semantik auf Variablen angewendet wird. Tatsächlich enthält der Standard keine Informationen zu allen Fragen des Multithreading. Bestimmte Plattformen wenden jedoch die Acquire- und Release-Semantik aufvolatile
Variablen an.[Update für C ++ 11]
Die C ++ 11 Standard jetzt tut acknowledge Multithreading direkt in das Speichermodell und dem lanuage, und es bietet Bibliothekseinrichtungen mit ihm in einer plattformunabhängigen Art und Weise zu behandeln. Die Semantik von hat sich jedoch
volatile
noch nicht geändert.volatile
ist immer noch kein Synchronisationsmechanismus. Bjarne Stroustrup sagt so viel in TCPPPL4E:[/ Update beenden]
Vor allem gilt die C ++ - Sprache selbst, wie sie im Standard von 2003 (und jetzt im Standard von 2011) definiert ist. Einige spezifische Plattformen fügen jedoch zusätzliche Funktionen oder Einschränkungen hinzu
volatile
. Zum Beispiel in MSVC 2010 (mindestens) Acquire and Release Semantik tun , um bestimmte Operationen auf geltenvolatile
Variablen. Aus dem MSDN :Sie können jedoch die Tatsache zur Kenntnis nehmen, dass, wenn Sie dem obigen Link folgen, in den Kommentaren einige Debatten darüber geführt werden, ob in diesem Fall die Semantik für den Erwerb / die Freigabe tatsächlich gilt oder nicht .
quelle
volatile
dieses Programm schreiben können, liegt dies daran, dass Sie auf den Schultern von Personen standen, die frühervolatile
Threading-Bibliotheken implementiert haben.volatile
tatsächlich funktioniert . Was @John gesagt hat, ist richtig , Ende der Geschichte. Es hat nichts mit Anwendungscode gegen Bibliothekscode oder "gewöhnlichen" gegen "gottähnliche allwissende Programmierer" zu tun.volatile
ist für die Synchronisation zwischen Threads unnötig und nutzlos. Threading-Bibliotheken können nicht implementiert werden in Bezug aufvolatile
; Es muss sich sowieso auf plattformspezifische Details stützen, und wenn Sie sich auf diese verlassen, brauchen Sie diese nicht mehrvolatile
.(Anmerkung des Herausgebers: In C ++ 11
volatile
ist es nicht das richtige Werkzeug für diesen Job und verfügt immer noch über UB für Datenrennen. Verwenden Sie esstd::atomic<bool>
mitstd::memory_order_relaxed
Lasten / Speichern, um dies ohne UB zu tun. Bei realen Implementierungen wird es auf dieselbe Weise kompiliert wievolatile
. Ich habe hinzugefügt Eine detailliertere Antwort und die Behebung der Missverständnisse in Kommentaren, dass schwach geordneter Speicher ein Problem für diesen Anwendungsfall sein könnte: Alle realen CPUs verfügen über einen kohärenten gemeinsam genutzten Speicher, sodass dies bei realen C ++ - Implementierungenvolatile
funktioniert tu es nichtEinige Diskussion in den Kommentaren scheint über andere Anwendungsfälle zu sprechen , wo man würde etwas stärker als entspannt atomics brauchen. Diese Antwort weist bereits darauf hin, dass
volatile
Sie keine Bestellung erhalten.)Flüchtig ist gelegentlich aus folgendem Grund nützlich: Dieser Code:
wird von gcc optimiert für:
Was offensichtlich falsch ist, wenn das Flag vom anderen Thread geschrieben wird. Beachten Sie, dass der Synchronisationsmechanismus ohne diese Optimierung wahrscheinlich funktioniert (abhängig vom anderen Code sind möglicherweise einige Speicherbarrieren erforderlich). In 1 Producer-1-Consumer-Szenario ist kein Mutex erforderlich.
Andernfalls ist das flüchtige Schlüsselwort zu seltsam, um verwendet werden zu können. Es bietet keine Speicherordnungsgarantie für flüchtige und nichtflüchtige Zugriffe und bietet keine atomaren Operationen. Das heißt, Sie erhalten vom Compiler keine Hilfe mit dem flüchtigen Schlüsselwort, außer dem deaktivierten Register-Caching .
quelle
volatile
verhindert nicht, dass Speicherzugriffe neu angeordnet werden.volatile
Zugriffe werden nicht in Bezug aufeinander neu angeordnet, bieten jedoch keine Garantie für eine Neuordnung in Bezug auf Nicht-volatile
Objekte und sind daher im Grunde auch als Flags unbrauchbar.volatile
.while (work_left) { do_piece_of_work(); if (cancel) break;}
Wenn der Abbruch innerhalb der Schleife neu angeordnet wird). Die Logik ist immer noch gültig. Ich hatte einen Code, der ähnlich funktionierte: Wenn der Hauptthread beendet werden soll, setzt er das Flag für andere Threads, aber nicht ...In C ++ 11 normalerweise nie
volatile
zum Threading verwenden, nur für MMIOAber TL: DR, es "funktioniert" wie atomar mit
mo_relaxed
Hardware mit kohärenten Caches (dh alles); Es reicht aus, Compiler daran zu hindern, Vars in Registern zu führen.atomic
Es sind keine Speicherbarrieren erforderlich, um Atomizität oder Sichtbarkeit zwischen Threads zu erzeugen, sondern nur, um den aktuellen Thread vor / nach einer Operation warten zu lassen, um eine Reihenfolge zwischen den Zugriffen dieses Threads auf verschiedene Variablen zu erstellen.mo_relaxed
braucht nie irgendwelche Barrieren, nur laden, lagern oder RMW.Für Roll-your-own atomics mit
volatile
(und Inline-asm für Barrieren) in den schlechten alten Tagen vor C ++ 11std::atomic
,volatile
war die einzige gute Möglichkeit , einige Dinge zur Arbeit zu kommen . Es hing jedoch von vielen Annahmen darüber ab, wie Implementierungen funktionierten, und wurde von keinem Standard garantiert.Zum Beispiel verwendet der Linux-Kernel immer noch seine eigenen handgerollten Atomics mit
volatile
, unterstützt jedoch nur einige spezifische C-Implementierungen (GNU C, Clang und möglicherweise ICC). Dies liegt zum Teil an GNU C-Erweiterungen und der Inline-Asm-Syntax und -Semantik, aber auch daran, dass einige Annahmen über die Funktionsweise von Compilern getroffen werden.Es ist fast immer die falsche Wahl für neue Projekte; Sie können
std::atomic
(mitstd::memory_order_relaxed
) verwenden, um einen Compiler dazu zu bringen, denselben effizienten Maschinencode auszugeben, mit dem Sie arbeiten könnenvolatile
.std::atomic
mitmo_relaxed
veraltetenvolatile
zum Einfädeln. (außer vielleicht, um Fehler bei der Fehloptimierung beiatomic<double>
einigen Compilern zu umgehen .)Die interne Implementierung von
std::atomic
On-Mainstream-Compilern (wie gcc und clang) wird nicht nurvolatile
intern verwendet. Compiler stellen die integrierten Funktionen für Atomlast, Speicher und RMW direkt zur Verfügung. (zB GNU C-__atomic
Builtins, die mit "einfachen" Objekten arbeiten.)Volatile ist in der Praxis verwendbar (aber nicht)
Das heißt,
volatile
ist in der Praxis für Dinge wie einexit_now
Flag auf allen (?) Vorhandenen C ++ - Implementierungen auf realen CPUs verwendbar , da CPUs funktionieren (kohärente Caches) und gemeinsame Annahmen darüber, wievolatile
es funktionieren soll. Aber sonst nicht viel und wird nicht empfohlen. Mit dieser Antwort soll erläutert werden, wie vorhandene CPUs und C ++ - Implementierungen tatsächlich funktionieren. Wenn Sie sich nicht darum kümmern, müssen Sie nur wissen, dassstd::atomic
mo_relaxedvolatile
für das Threading veraltet ist .(Der ISO C ++ - Standard ist ziemlich vage und sagt nur, dass
volatile
Zugriffe streng nach den Regeln der abstrakten C ++ - Maschine ausgewertet und nicht wegoptimiert werden sollten. Angesichts der Tatsache, dass echte Implementierungen den Speicheradressraum der Maschine verwenden, um den C ++ - Adressraum zu modellieren, Dies bedeutet, dassvolatile
Lesevorgänge und Zuweisungen kompiliert werden müssen, um Anweisungen zu laden / speichern, um auf die Objektdarstellung im Speicher zuzugreifen.)Wie eine andere Antwort hervorhebt, ist ein
exit_now
Flag ein einfacher Fall von Kommunikation zwischen Threads, für den keine Synchronisierung erforderlich ist : Es wird nicht veröffentlicht, dass Array-Inhalte bereit sind oder ähnliches. Nur ein Geschäft, das sofort durch eine nicht optimierte Abwesenheitsladung in einem anderen Thread bemerkt wird.Ohne flüchtig oder atomar erlaubt die Als-ob-Regel und die Annahme, dass kein Datenrenn-UB vorliegt, einem Compiler, es in asm zu optimieren, das das Flag nur einmal überprüft , bevor es in eine Endlosschleife eintritt (oder nicht). Genau das passiert im wirklichen Leben für echte Compiler. (Und optimieren Sie normalerweise viel davon,
do_stuff
weil die Schleife nie beendet wird, sodass späterer Code, der möglicherweise das Ergebnis verwendet hat, nicht erreichbar ist, wenn wir in die Schleife eintreten.)Das Multithreading-Programm, das im optimierten Modus steckt, aber normal in -O0 ausgeführt wird, ist ein Beispiel (mit Beschreibung der ASM-Ausgabe von GCC), wie genau dies mit GCC auf x86-64 geschieht. Auch die MCU-Programmierung - C ++ O2-Optimierung unterbricht die Schleife der Elektronik. SE zeigt ein weiteres Beispiel.
Wir wollen normalerweise aggressive Optimierungen, die CSE und Hoist aus Schleifen laden, auch für globale Variablen.
Vor C ++ 11 gab
volatile bool exit_now
es eine Möglichkeit , dies wie beabsichtigt zu machen (bei normalen C ++ - Implementierungen). In C ++ 11 gilt Data Race UB jedoch weiterhin für,volatile
sodass der ISO-Standard nicht garantiert , dass er überall funktioniert, selbst wenn HW-kohärente Caches vorausgesetzt werden.Beachten Sie, dass bei breiteren Typen
volatile
keine Garantie für mangelndes Reißen gegeben ist. Ich habe diese Unterscheidung hier ignoriert,bool
da sie bei normalen Implementierungen kein Problem darstellt. Aber das ist auch ein Teil dessen, warumvolatile
UB immer noch dem Datenrennen unterliegt, anstatt einem entspannten Atom zu entsprechen.Beachten Sie, dass "wie beabsichtigt" nicht bedeutet, dass der Thread darauf
exit_now
wartet, dass der andere Thread tatsächlich beendet wird. Oder sogar, dass es darauf wartet, dass der flüchtigeexit_now=true
Speicher überhaupt global sichtbar ist, bevor mit späteren Operationen in diesem Thread fortgefahren wird. (atomic<bool>
Mit der Standardeinstellungmo_seq_cst
würde es warten, bis später seq_cst mindestens geladen wird. Bei vielen ISAs erhalten Sie nach dem Speichern nur eine vollständige Barriere.)C ++ 11 bietet eine Nicht-UB-Methode, die dieselbe kompiliert
Ein "Weiter laufen" - oder "Jetzt beenden" -Flag sollte
std::atomic<bool> flag
mit verwendet werdenmo_relaxed
Verwenden von
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
Sie erhalten genau das gleiche Ergebnis (ohne teure Barriereanweisungen), von dem Sie erhalten würden
volatile flag
.Sie können nicht nur reißen, sondern
atomic
auch in einem Thread speichern und in einem anderen ohne UB laden, sodass der Compiler die Last nicht aus einer Schleife heben kann. (Die Annahme, dass kein Datenrenn-UB vorhanden ist, ermöglicht die aggressiven Optimierungen, die wir für nichtatomare, nichtflüchtige Objekte wünschen.) Diese Funktion von entsprichtatomic<T>
weitgehend dervolatile
für reine Lasten und reine Speicher.atomic<T>
Machen Sie auch+=
und so weiter zu atomaren RMW-Operationen (wesentlich teurer als eine atomare Last in eine temporäre, betreiben Sie dann einen separaten Atomspeicher. Wenn Sie keine atomare RMW möchten, schreiben Sie Ihren Code mit einer lokalen temporären).Mit der Standardbestellung
seq_cst
, von der Sie erhaltenwhile(!flag)
, werden auch Bestellgarantien hinzugefügt. nichtatomare Zugriffe und auf andere atomare Zugriffe.(Theoretisch schließt der ISO C ++ - Standard eine Optimierung der Atomik zur Kompilierungszeit nicht aus. In der Praxis tun dies Compiler jedoch nicht, da nicht gesteuert werden kann, wann dies nicht in Ordnung ist. Es gibt einige Fälle, in denen dies
volatile atomic<T>
möglicherweise nicht der Fall ist Genug Kontrolle über die Optimierung von Atomics haben, wenn Compiler optimiert haben, also Compiler vorerst nicht. Siehe Warum Compiler keine redundanten std :: atomic-Schreibvorgänge zusammenführen? Beachten Sie, dass wg21 / p0062 davon abrät,volatile atomic
im aktuellen Code die Optimierung von zu verwenden Atomics.)volatile
funktioniert tatsächlich auf echten CPUs (aber immer noch nicht verwenden)auch bei schwach geordneten Speichermodellen (nicht x86) . Aber verwenden Sie nicht eigentlich ist es, die Verwendung
atomic<T>
mitmo_relaxed
statt !! In diesem Abschnitt geht es darum, Missverständnisse über die Funktionsweise realer CPUs auszuräumen und nicht zu rechtfertigenvolatile
. Wenn Sie sperrenlosen Code schreiben, ist Ihnen wahrscheinlich die Leistung wichtig. Das Verständnis der Caches und der Kosten für die Kommunikation zwischen Threads ist normalerweise wichtig für eine gute Leistung.Echte CPUs verfügen über kohärente Caches / gemeinsam genutzten Speicher: Nachdem ein Speicher von einem Kern global sichtbar wird, kann kein anderer Kern einen veralteten Wert laden . (Siehe auch Mythen Programmierer glauben an CPU-Caches, in denen es um flüchtige Java- Dateien geht, die C ++
atomic<T>
mit der Speicherreihenfolge seq_cst entsprechen.)Wenn ich Laden sage , meine ich eine ASM-Anweisung, die auf den Speicher zugreift. Dies
volatile
stellt ein Zugriff sicher und ist nicht dasselbe wie die Konvertierung einer nichtatomaren / nichtflüchtigen C ++ - Variablen von lWert in rWert. (zBlocal_tmp = flag
oderwhile(!flag)
).Das einzige, was Sie besiegen müssen, sind Optimierungen zur Kompilierungszeit, die nach der ersten Überprüfung überhaupt nicht neu geladen werden. Jedes Laden + Überprüfen bei jeder Iteration ist ohne Bestellung ausreichend. Ohne Synchronisation zwischen diesem Thread und dem Haupt-Thread ist es nicht sinnvoll, darüber zu sprechen, wann genau der Speicher passiert ist, oder über die Reihenfolge des Ladevorgangs. andere Operationen in der Schleife. Nur wenn es für diesen Thread sichtbar ist, kommt es darauf an. Wenn das Flag exit_now gesetzt ist, beenden Sie das Programm. Die Latenz zwischen den Kernen auf einem typischen x86-Xeon kann zwischen separaten physischen Kernen etwa 40 ns betragen .
Theoretisch: C ++ - Threads auf Hardware ohne kohärente Caches
Ich sehe keine Möglichkeit, dass dies mit reinem ISO C ++ aus der Ferne effizient sein könnte, ohne dass der Programmierer explizite Löschvorgänge im Quellcode durchführen muss.
Theoretisch könnten Sie eine C ++ - Implementierung auf einem Computer haben, der nicht so ist, und vom Compiler generierte explizite Flushes erfordern, um Dinge für andere Threads auf anderen Kernen sichtbar zu machen . (Oder für Lesevorgänge, um keine möglicherweise veraltete Kopie zu verwenden). Der C ++ - Standard macht dies nicht unmöglich, aber das Speichermodell von C ++ ist darauf ausgelegt, auf kohärenten Shared-Memory-Computern effizient zu sein. Beispielsweise spricht der C ++ - Standard sogar von "Lese-Lese-Kohärenz", "Schreib-Lese-Kohärenz" usw. Ein Hinweis im Standard weist sogar auf die Verbindung zur Hardware hin:
Es gibt keinen Mechanismus für ein
release
Geschäft, um sich selbst und einige ausgewählte Adressbereiche zu leeren: Es müsste alles synchronisieren, da es nicht wissen würde, was andere Threads lesen möchten, wenn ihre Erfassungslast diesen Release-Speicher sah (a Release-Sequenz, die eine Thread-Before-Beziehung zwischen Threads herstellt und garantiert, dass frühere nicht-atomare Operationen, die vom Schreib-Thread ausgeführt werden, jetzt sicher gelesen werden können. Es sei denn, sie wurden nach dem Release-Speicher weiter geschrieben ...) Oder Compiler hätten dies getan um wirklich klug zu sein und zu beweisen, dass nur wenige Cache-Zeilen geleert werden müssen.Verwandte: meine Antwort auf Ist mov + mfence auf NUMA sicher? geht detailliert auf die Nichtexistenz von x86-Systemen ohne kohärenten gemeinsamen Speicher ein. Ebenfalls verwandt: Lädt und speichert die Neuordnung in ARM, um mehr über das Laden / Speichern am selben Ort zu erfahren.
Es sind Ich denke , Cluster mit nicht-kohärenten Shared Memory, aber sie sind nicht Single-System-Image - Maschinen. Jede Kohärenzdomäne führt einen separaten Kernel aus, sodass Sie keine Threads eines einzelnen C ++ - Programms darauf ausführen können. Stattdessen führen Sie separate Instanzen des Programms aus (jede mit ihrem eigenen Adressraum: Zeiger in einer Instanz sind in der anderen nicht gültig).
Um sie dazu zu bringen, über explizite Löschvorgänge miteinander zu kommunizieren, verwenden Sie normalerweise MPI oder eine andere API zur Nachrichtenübermittlung, damit das Programm angibt, welche Adressbereiche gelöscht werden müssen.
Echte Hardware läuft nicht
std::thread
über Cache-Kohärenzgrenzen hinweg:Es gibt einige asymmetrische ARM-Chips mit gemeinsam genutztem physischen Adressraum, jedoch nicht gemeinsam nutzbaren Cache-Domänen. Also nicht kohärent. (zB Kommentarthread ein A8-Kern und ein Cortex-M3 wie TI Sitara AM335x).
Auf diesen Kernen würden jedoch unterschiedliche Kernel ausgeführt, nicht ein einziges Systemabbild, das Threads über beide Kerne ausführen könnte. Mir sind keine C ++ - Implementierungen bekannt, die
std::thread
Threads über CPU-Kerne ohne kohärente Caches ausführen .Speziell für ARM generieren GCC und Clang Code, sofern alle Threads in derselben gemeinsam nutzbaren Domäne ausgeführt werden. In der Tat heißt es im ARMv7 ISA-Handbuch
Ein nicht kohärenter gemeinsamer Speicher zwischen verschiedenen Domänen ist also nur eine Sache für die explizite systemspezifische Verwendung von gemeinsam genutzten Speicherbereichen für die Kommunikation zwischen verschiedenen Prozessen unter verschiedenen Kerneln.
Siehe auch diese CoreCLR- Diskussion über Code-Gen unter Verwendung von
dmb ish
(Inner Shareable Barrier) vs.dmb sy
(System) Speicherbarrieren in diesem Compiler.Ich mache die Behauptung, dass keine C ++ - Implementierung für andere ISA
std::thread
über Kerne mit nicht kohärenten Caches läuft . Ich habe keinen Beweis dafür, dass es keine solche Implementierung gibt, aber es scheint höchst unwahrscheinlich. Sofern Sie nicht auf ein bestimmtes exotisches Stück HW abzielen, das auf diese Weise funktioniert, sollte Ihr Denken über die Leistung eine MESI-ähnliche Cache-Kohärenz zwischen allen Threads voraussetzen. (Am bestenatomic<T>
auf eine Weise verwenden, die die Richtigkeit garantiert!)Kohärente Caches machen es einfach
Aber auf einem Multi-Core - System mit kohärentem Caches, Release-Store - Implementierung bedeutet nur , Ordnung in der Cache - commit für diesen speichert Thread, keine ausdrückliche Spülung tun. ( https://preshing.com/20120913/acquire-and-release-semantics/ und https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (Und ein Erfassungsladen bedeutet, den Zugriff auf den Cache im anderen Kern zu bestellen).
Ein Speicherbarrierebefehl blockiert nur das Laden und / oder Speichern des aktuellen Threads, bis der Speicherpuffer leer ist. das geht immer so schnell wie möglich von alleine. ( Stellt eine Speicherbarriere sicher, dass die Cache-Kohärenz abgeschlossen ist? Behebt dieses Missverständnis). Wenn Sie also keine Bestellung benötigen, ist es in Ordnung, nur die Sichtbarkeit in anderen Threads zu veranlassen
mo_relaxed
. (Und so ist es auchvolatile
, aber tu das nicht.)Siehe auch C / C ++ 11-Zuordnungen zu Prozessoren
Unterhaltsame Tatsache: Auf x86 ist jeder ASM-Speicher ein Release-Speicher, da das x86-Speichermodell im Grunde genommen aus einem Speicherpuffer (mit Speicherweiterleitung) besteht.
Halbbezogener Re: Store-Puffer, globale Sichtbarkeit und Kohärenz: C ++ 11 garantiert nur sehr wenig. Die meisten echten ISAs (außer PowerPC) garantieren, dass sich alle Threads auf die Reihenfolge des Auftretens von zwei Speichern durch zwei andere Threads einigen können. (In der formalen Terminologie des Speichermodells der Computerarchitektur sind sie "atomar mit mehreren Kopien").
Ein weiteres Missverständnis ist, dass Speicherzaun-Anweisungen erforderlich sind, um den Speicherpuffer zu leeren, damit andere Kerne unsere Speicher überhaupt sehen können . Tatsächlich versucht der Speicherpuffer immer, sich so schnell wie möglich zu entleeren (Commit in den L1d-Cache), da er sonst die Ausführung auffüllt und blockiert. Eine vollständige Barriere / ein vollständiger Zaun blockiert den aktuellen Thread, bis der Speicherpuffer leer ist , sodass unsere späteren Ladevorgänge in der globalen Reihenfolge nach unseren früheren Speichern angezeigt werden.
(x86 dringend asm Speichermodell Mittel angeordnet , dass
volatile
auf x86 kann am Ende Ihnen näher an gebenmo_acq_rel
, außer dass Compile-Zeit mit nicht-atomaren Variablen Nachbestellung kann immer noch passieren. Aber die meisten nicht-x86 hat Speichermodelle-schwach bestellt sovolatile
undrelaxed
ist etwa so schwach wiemo_relaxed
erlaubt.)quelle
atomic
dazu führen könnte, dass unterschiedliche Threads unterschiedliche Werte für dieselbe Variable im Cache haben . / Gesichtspalme. Im Cache nein, in den CPU- Registern ja (mit nichtatomaren Variablen); CPUs verwenden einen kohärenten Cache. Ich wünschte, andere Fragen zu SO wären nicht voller Erklärungenatomic
, die falsche Vorstellungen über die Funktionsweise von CPUs verbreiten. (Weil dies aus Leistungsgründen eine nützliche Sache ist und auch erklärt, warum die ISO C ++ - Atomregeln so geschrieben sind, wie sie sind.)Einmal argumentierte ein Interviewer, der auch glaubte, dass flüchtig nutzlos ist, mit mir, dass die Optimierung keine Probleme verursachen würde, und bezog sich auf verschiedene Kerne mit separaten Cache-Zeilen und all dem (verstand nicht wirklich, worauf er sich genau bezog). Wenn dieser Code mit -O3 unter g ++ (g ++ -O3 thread.cpp -lpthread) kompiliert wird, zeigt er undefiniertes Verhalten. Grundsätzlich funktioniert es einwandfrei, wenn der Wert vor der while-Prüfung festgelegt wird, und wenn nicht, geht er in eine Schleife, ohne sich die Mühe zu machen, den Wert abzurufen (der tatsächlich vom anderen Thread geändert wurde). Grundsätzlich glaube ich, dass der Wert von checkValue nur einmal in das Register abgerufen und unter der höchsten Optimierungsstufe nie wieder überprüft wird. Wenn es vor dem Abruf auf true gesetzt ist, funktioniert es einwandfrei und wenn nicht, geht es in eine Schleife. Bitte korrigieren Sie mich, wenn ich falsch liege.
quelle
volatile
? Ja, dieser Code ist UB - aber es ist auch UB mitvolatile
.Sie benötigen flüchtige und möglicherweise sperrende.
volatile teilt dem Optimierer mit, dass sich der Wert asynchron ändern kann
liest jedes Mal Flag um die Schleife.
Wenn Sie die Optimierung deaktivieren oder jede Variable flüchtig machen, verhält sich ein Programm gleich, aber langsamer. flüchtig bedeutet nur: „Ich weiß, dass Sie es vielleicht gerade gelesen haben und wissen, was es sagt, aber wenn ich sage, lesen Sie es, dann lesen Sie es.
Das Sperren ist Teil des Programms. Wenn Sie also Semaphoren implementieren, müssen diese unter anderem volatil sein. (Versuchen Sie es nicht, es ist schwer, wird wahrscheinlich einen kleinen Assembler oder das neue atomare Zeug brauchen, und es wurde bereits getan.)
quelle
volatile
ist nicht wirklich nützlich , auch in diesem Fall. Aber beschäftigtes Warten ist eine gelegentlich nützliche Technik.volatile
, was "keine Neuordnung" bedeutet. Sie hoffen, dass dies bedeutet, dass die Stores in der Programmreihenfolge global (für andere Threads) sichtbar werden. Das ist wasatomic<T>
mitmemory_order_release
oderseq_cst
gibt dir. Sie erhalten jedochvolatile
nur die Garantie, dass keine Neuordnung zur Kompilierungszeit erfolgt : Jeder Zugriff wird in der Programmreihenfolge im asm angezeigt. Nützlich für einen Gerätetreiber. Und nützlich für die Interaktion mit einem Interrupt-Handler, Debugger oder Signal-Handler auf dem aktuellen Kern / Thread, jedoch nicht für die Interaktion mit anderen Kernen.volatile
In der Praxis reicht es aus, einkeep_running
Flag zu überprüfen, wie Sie es hier tun: Echte CPUs haben immer kohärente Caches, die kein manuelles Leeren erfordern. Aber es gibt keinen Grund zu empfehlenvolatile
überatomic<T>
mitmo_relaxed
; Du wirst den gleichen Asm bekommen.