Wie in dieser Antwort, die ich kürzlich gepostet habe, gezeigt wurde, bin ich verwirrt über den Nutzen (oder das Fehlen davon) volatile
in Multithread-Programmierkontexten.
Mein Verständnis ist folgendes: Jedes Mal, wenn eine Variable außerhalb des Kontrollflusses eines Codes, der darauf zugreift, geändert werden kann, sollte diese Variable als solche deklariert werden volatile
. Signalhandler, E / A-Register und Variablen, die von einem anderen Thread geändert wurden, bilden solche Situationen.
Wenn Sie also ein globales int foo
haben und foo
von einem Thread gelesen und von einem anderen Thread atomar gesetzt werden (wahrscheinlich unter Verwendung einer geeigneten Maschinenanweisung), sieht der lesende Thread diese Situation genauso wie eine Variable, die von einem Signalhandler oder optimiert wurde geändert durch eine externe Hardwarebedingung und foo
sollte daher deklariert werden volatile
(oder für Multithread-Situationen mit speicherbegrenzter Last zugegriffen werden, was wahrscheinlich eine bessere Lösung ist).
Wie und wo irre ich mich?
Antworten:
Das Problem
volatile
in einem Multithread-Kontext ist, dass es nicht alle Garantien bietet, die wir brauchen. Es hat einige Eigenschaften, die wir brauchen, aber nicht alle, also können wir uns nichtvolatile
alleine verlassen .Die Grundelemente, die wir für die verbleibenden Eigenschaften verwenden müssten, bieten jedoch auch diejenigen, die
volatile
dies tun, sodass dies praktisch nicht erforderlich ist.Für threadsichere Zugriffe auf gemeinsam genutzte Daten benötigen wir eine Garantie, dass:
volatile
Variable als Flag, um anzugeben, ob einige Daten zum Lesen bereit sind oder nicht. In unserem Code setzen wir einfach das Flag, nachdem wir die Daten vorbereitet haben, damit alles gut aussieht . Aber was ist, wenn die Anweisungen neu angeordnet werden, sodass das Flag zuerst gesetzt wird ?volatile
garantiert den ersten Punkt. Es garantiert auch, dass keine Neuordnung zwischen verschiedenen flüchtigen Lese- / Schreibvorgängen erfolgt . Allevolatile
Speicherzugriffe erfolgen in der angegebenen Reihenfolge. Das ist alles, was wir für das benötigen, wofürvolatile
vorgesehen ist: das Manipulieren von E / A-Registern oder speicherabgebildeter Hardware, aber es hilft uns nicht bei Multithread-Code, bei dem dasvolatile
Objekt häufig nur zum Synchronisieren des Zugriffs auf nichtflüchtige Daten verwendet wird. Diese Zugriffe können weiterhin relativ zuvolatile
denen neu angeordnet werden.Die Lösung zur Verhinderung einer Neuordnung besteht in der Verwendung einer Speicherbarriere , die sowohl dem Compiler als auch der CPU anzeigt, dass über diesen Punkt kein Speicherzugriff neu angeordnet werden darf . Durch das Platzieren solcher Barrieren um unseren Zugriff auf flüchtige Variablen wird sichergestellt, dass selbst nichtflüchtige Zugriffe nicht über den flüchtigen Zugriff hinweg neu angeordnet werden, sodass wir threadsicheren Code schreiben können.
Speicherbarrieren stellen jedoch auch sicher, dass alle ausstehenden Lese- / Schreibvorgänge ausgeführt werden, wenn die Barriere erreicht ist, sodass wir effektiv alles haben, was wir selbst benötigen, was
volatile
unnötig wird. Wir können dasvolatile
Qualifikationsmerkmal einfach vollständig entfernen .Seit C ++ 11
std::atomic<T>
geben uns atomare Variablen ( ) alle relevanten Garantien.quelle
volatile
nicht stark genug sind, um nützlich zu sein.volatile
sich jedoch neu als vollständige Speicherbarriere (verhindert eine Neuordnung). Dies ist nicht Teil des Standards, daher können Sie sich bei tragbarem Code nicht auf dieses Verhalten verlassen.volatile
ist für die Multithread-Programmierung immer nutzlos. (Außer unter Visual Studio, wo flüchtige ist die Speicherbarriere - Erweiterung.)Sie können dies auch in der Linux-Kernel-Dokumentation berücksichtigen .
quelle
volatile
ist das auch sinnlos. In allen Fällen ist das Verhalten "Aufruf einer Funktion, deren Körper nicht sichtbar ist" korrekt.Ich glaube nicht, dass Sie sich irren - flüchtig ist notwendig, um sicherzustellen, dass Thread A die Wertänderung sieht, wenn der Wert durch etwas anderes als Thread A geändert wird. Nach meinem Verständnis ist flüchtig im Grunde eine Möglichkeit, dies zu sagen Compiler "Zwischenspeichern Sie diese Variable nicht in einem Register, sondern lesen / schreiben Sie sie bei jedem Zugriff aus dem RAM-Speicher".
Die Verwirrung ist, dass Volatilität nicht ausreicht, um eine Reihe von Dingen zu implementieren. Insbesondere verwenden moderne Systeme mehrere Caching-Ebenen, moderne Multi-Core-CPUs führen zur Laufzeit einige ausgefallene Optimierungen durch, und moderne Compiler führen zur Kompilierungszeit einige ausgefallene Optimierungen durch, die alle dazu führen können, dass verschiedene Nebenwirkungen unterschiedlich auftreten Bestellung aus der Bestellung, die Sie erwarten würden, wenn Sie nur den Quellcode betrachten.
So flüchtig ist in Ordnung, solange Sie bedenken, dass die "beobachteten" Änderungen in der flüchtigen Variablen möglicherweise nicht genau zu dem Zeitpunkt auftreten, zu dem Sie glauben, dass sie eintreten werden. Versuchen Sie insbesondere nicht, flüchtige Variablen zu verwenden, um Operationen über Threads hinweg zu synchronisieren oder zu ordnen, da dies nicht zuverlässig funktioniert.
Persönlich ist meine Hauptverwendung (nur?) Für das flüchtige Flag ein "PleaseGoAwayNow" -Boolescher Wert. Wenn ich einen Worker-Thread habe, der kontinuierlich Schleifen durchläuft, muss er den flüchtigen Booleschen Wert bei jeder Iteration der Schleife überprüfen und beenden, wenn der Boolesche Wert jemals wahr ist. Der Hauptthread kann dann den Arbeitsthread sicher bereinigen, indem er den Booleschen Wert auf true setzt und dann pthread_join () aufruft, um zu warten, bis der Arbeitsthread verschwunden ist.
quelle
mutex_lock
(und jede andere Bibliotheksfunktion), den Status der Flag-Variablen zu ändern.SCHED_FIFO
, eine höhere statische Priorität als andere Prozesse / Threads im System, genügend Kerne sollten durchaus möglich sein. Unter Linux können Sie festlegen, dass der Echtzeitprozess 100% der CPU-Zeit beanspruchen kann. Sie wechseln niemals den Kontext, wenn kein Thread / Prozess mit höherer Priorität vorhanden ist, und blockieren niemals durch E / A. Der Punkt ist jedoch, dass C / C ++volatile
nicht dazu gedacht ist, eine ordnungsgemäße Semantik für die gemeinsame Nutzung / Synchronisierung von Daten zu erzwingen. Ich finde die Suche nach Sonderfällen, um zu beweisen, dass falscher Code manchmal funktionieren könnte, nutzlose Übung.volatile
ist nützlich (wenn auch nicht ausreichend), um das Grundkonstrukt eines Spinlock-Mutex zu implementieren, aber sobald Sie das (oder etwas Überlegenes) haben, brauchen Sie kein anderesvolatile
.Die typische Art der Multithread-Programmierung besteht nicht darin, jede gemeinsam genutzte Variable auf Maschinenebene zu schützen, sondern Schutzvariablen einzuführen, die den Programmfluss steuern. Anstelle von
volatile bool my_shared_flag;
dir sollte habenDies kapselt nicht nur den "schwierigen Teil", sondern ist grundsätzlich notwendig: C enthält keine atomaren Operationen, die zur Implementierung eines Mutex erforderlich sind; es muss nur
volatile
zusätzliche Garantien für den normalen Betrieb geben.Jetzt hast du so etwas:
my_shared_flag
muss nicht volatil sein, obwohl es nicht zwischenspeicherbar ist, weil&
Bediener) aufgenommen worden sein.pthread_mutex_lock
ist eine Bibliotheksfunktion.pthread_mutex_lock
diese Referenz irgendwie erhält.pthread_mutex_lock
das Shared Flag geändert wird !volatile
, obwohl in diesem Zusammenhang sinnvoll, ist irrelevant.quelle
Dein Verständnis ist wirklich falsch.
Die Eigenschaft der flüchtigen Variablen lautet "Lesen von und Schreiben in diese Variable sind Teil des wahrnehmbaren Verhaltens des Programms". Das heißt, dieses Programm funktioniert (bei entsprechender Hardware):
Das Problem ist, dass dies nicht die Eigenschaft ist, die wir von threadsicherem irgendetwas wollen.
Ein thread-sicherer Zähler wäre beispielsweise nur (Linux-Kernel-ähnlicher Code, kenne das c ++ 0x-Äquivalent nicht):
Dies ist atomar, ohne eine Gedächtnisbarriere. Sie sollten sie bei Bedarf hinzufügen. Das Hinzufügen von volatile würde wahrscheinlich nicht helfen, da es den Zugriff auf den Code in der Nähe nicht in Beziehung setzen würde (z. B. das Anhängen eines Elements an die Liste, die der Zähler zählt). Natürlich müssen Sie den Zähler nicht außerhalb Ihres Programms inkrementieren, und Optimierungen sind immer noch wünschenswert, z.
kann noch optimiert werden
Wenn das Optimierungsprogramm intelligent genug ist (es ändert nichts an der Semantik des Codes).
quelle
Damit Ihre Daten in einer gleichzeitigen Umgebung konsistent sind, müssen zwei Bedingungen erfüllt sein:
1) Atomizität dh wenn ich einige Daten lese oder in den Speicher schreibe, werden diese Daten in einem Durchgang gelesen / geschrieben und können nicht unterbrochen oder konkurriert werden, z. B. aufgrund eines Kontextwechsels
2) Konsistenz , dh die Reihenfolge der Lese- / Schreib - ops werden muß gesehen das gleiche zwischen mehreren gleichzeitigen Umgebungen sein -, dass Threads, Maschinen usw.
volatile passt zu keinem der oben genannten Punkte - oder insbesondere enthält der c- oder c ++ - Standard, wie sich volatile verhalten soll, keinen der oben genannten Punkte.
In der Praxis ist dies sogar noch schlimmer, da einige Compiler (wie der Intel Itanium-Compiler) versuchen, ein Element des gleichzeitigen Zugriffssicherheitsverhaltens zu implementieren (dh durch Sicherstellen von Speicherzäunen). Es besteht jedoch keine Konsistenz zwischen den Compiler-Implementierungen, und der Standard verlangt dies nicht der Umsetzung in erster Linie.
Das Markieren einer Variablen als flüchtig bedeutet nur, dass Sie den Wert jedes Mal in den und aus dem Speicher leeren müssen, was in vielen Fällen Ihren Code nur verlangsamt, da Sie im Grunde Ihre Cache-Leistung beeinträchtigt haben.
c # und Java AFAIK beheben dies, indem sie die flüchtigen Bedingungen 1) und 2) einhalten. Dies gilt jedoch nicht für c / c ++ - Compiler.
Lesen Sie dies für eine eingehendere (wenn auch nicht unvoreingenommene) Diskussion zu diesem Thema
quelle
Die FAQ zu comp.programming.threads enthält eine klassische Erklärung von Dave Butenhof:
Herr Butenhof behandelt in diesem Usenet-Beitrag einen Großteil des gleichen Themas :
All das gilt auch für C ++.
quelle
Dies ist alles, was "flüchtig" tut: "Hey Compiler, diese Variable könnte sich in jedem Moment (bei jedem Takt) ändern, selbst wenn KEINE LOKALEN ANWEISUNGEN darauf einwirken. Speichern Sie diesen Wert NICHT in einem Register."
Das ist es. Es teilt dem Compiler mit, dass Ihr Wert flüchtig ist. Dieser Wert kann jederzeit durch externe Logik (ein anderer Thread, ein anderer Prozess, der Kernel usw.) geändert werden. Es existiert mehr oder weniger ausschließlich, um Compiler-Optimierungen zu unterdrücken, die einen Wert in einem Register stillschweigend zwischenspeichern, der für JEDEN Cache von Natur aus unsicher ist.
Möglicherweise stoßen Sie auf Artikel wie "Dr. Dobbs", die als Allheilmittel für die Multithread-Programmierung flüchtig sind. Sein Ansatz ist nicht völlig unbegründet, aber er hat den grundlegenden Fehler, die Benutzer eines Objekts für seine Thread-Sicherheit verantwortlich zu machen, was tendenziell dieselben Probleme aufweist wie andere Verstöße gegen die Kapselung.
quelle
Gemäß meinem alten C-Standard ist „Was einen Zugriff auf ein Objekt mit einem flüchtig qualifizierten Typ ausmacht, ist implementierungsdefiniert“ . C-Compiler-Autoren hätten sich also für "flüchtig" entscheiden können, was "thread-sicherer Zugriff in einer Umgebung mit mehreren Prozessen" bedeutet . Aber sie haben es nicht getan.
Stattdessen wurden die Vorgänge, die erforderlich sind, um einen kritischen Abschnittsthread in einer Mehrkern-Umgebung mit gemeinsamem Speicher und mehreren Prozessen sicher zu machen, als neue implementierungsdefinierte Funktionen hinzugefügt. Befreit von der Anforderung, dass "flüchtig" atomaren Zugriff und Zugriffsreihenfolge in einer Umgebung mit mehreren Prozessen bereitstellen würde, priorisierten die Compiler-Autoren die Code-Reduktion gegenüber der von der historischen Implementierung abhängigen "flüchtigen" Semantik.
Dies bedeutet, dass Dinge wie "flüchtige" Semaphoren um kritische Codeabschnitte, die auf neuer Hardware mit neuen Compilern nicht funktionieren, möglicherweise einmal mit alten Compilern auf alter Hardware funktioniert haben und alte Beispiele manchmal nicht falsch sind, sondern nur alt.
quelle
volatile
zu tun ist, damit ein Betriebssystem auf eine Weise geschrieben werden kann, die hardwareabhängig, aber vom Compiler unabhängig ist. Die Forderung, dass Programmierer implementierungsabhängige Funktionen verwenden müssen, anstatt dievolatile
erforderlichen Arbeiten auszuführen, untergräbt den Zweck eines Standards.