Warum flüchtig ist nicht sinnvoll betrachtet in Multithreaded C oder C ++ Programmierung?
-
21-09-2019 - |
Frage
Wie gezeigt in dieser Antwort ich vor kurzem geschrieben, ich scheine verwirrt über das Dienstprogramm (oder deren fehlen) von volatile
in Multi-Threaded-Programmierung Kontexten werden.
Mein Verständnis ist dies: jederzeit eine Variable außerhalb des Ablaufs der Steuerung eines Stückes Code geändert werden kann, den Zugriff darauf, dass Variable sollte seine volatile
deklariert werden. Signal-Handler, I / O-Register und Variablen, die von einem anderen Thread modifiziert alle solche Situationen dar.
Wenn Sie also eine globale int foo
haben, und foo
wird von einem Thread und setzen atomar von einem anderen Thread (wahrscheinlich unter Verwendung eines geeigneten Maschinenbefehl), der Lese Thread sieht in dieser Situation auf die gleiche Weise lesen sieht es eine Variable gezwickt durch eine Signalbehandlungsroutine oder durch einen externen Hardware-Zustand geändert und somit sollte foo
volatile
(oder für multithreaded Situationen abgerufen mit Memory-eingezäunten Last, was wahrscheinlich ist ein bessere Lösung) deklariert werden.
Wie und wo bin ich falsch?
Lösung
Das Problem mit volatile
in einem multithreaded Zusammenhang ist, dass es nicht schaffen, alle die Garantien, die wir brauchen. Es hat ein paar Eigenschaften, die wir brauchen, aber nicht alle von ihnen, so dass wir nicht auf volatile
verlassen können allein .
Allerdings sind die Primitiven, wir würden für die Verwendung haben die verbleibenden Eigenschaften bieten auch diejenigen, die volatile
tut, so ist es effektiv nicht notwendig ist.
Für Thread-sichere Zugriffe auf gemeinsam genutzte Daten, müssen wir eine Garantie dafür, dass:
- der Lese- / Schreib tatsächlich passiert (dass der Compiler nicht nur den Wert speichern, in einem Register statt und aufzuschieben, bis viel später Hauptspeicher Aktualisierung)
- , dass keine Umordnung stattfindet. Es sei angenommen, dass wir eine
volatile
Variable als Flag verwenden, um anzuzeigen, ob oder ob nicht einige Daten bereit ist, gelesen zu werden. In unserem Code, setzen wir einfach die Fahne nach der Vorbereitung von Daten, so dass alle Aussehen in Ordnung. Was aber, wenn die Befehle neu geordnet werden, um das Flag gesetzt ist erste
volatile
macht den ersten Punkt garantieren. Es gewährleistet auch, dass keine Umordnung auftritt zwischen verschiedenen flüchtigen liest / schreibt . Alle volatile
Speicherzugriffe werden in der Reihenfolge auftreten, in der sie angegeben sind. Das ist alles, was wir brauchen für das, was volatile
für gedacht: I / O-Register oder Memory-Mapped-Hardware zu manipulieren, aber es hat uns nicht in multithreaded Code helfen, wo das volatile
Objekt oft nur zu synchronisieren Zugang zu nicht-flüchtigen Daten verwendet wird. Diese Zugriffe können nach wie vor zu den volatile
diejenigen neu geordnet relativ werden.
Die Lösung Umordnung zu verhindern, ist ein zu verwenden Speicherbarriere , die beide an den Compiler und die CPU gibt an, dass kein Speicherzugriff über diesen Punkt neu angeordnet werden können, . Platzieren solcher Barrieren um unsere flüchtigen variablen Zugriff sichergestellt, dass auch nicht-flüchtige Zugriffe werden nicht über den Flüchtigen erst nachbestellt werden, so dass wir zu schreiben thread-safe-Code.
Allerdings Speicherbarrieren auch sicherstellen, dass alle anstehenden liest / schreibt ausgeführt werden, wenn die Barriere erreicht ist, so dass es uns effektiv alles gibt uns selbst brauchen, volatile
überflüssig macht. Wir können nur die volatile
Qualifier vollständig entfernen.
Da C ++ 11, Atom Variablen (std::atomic<T>
) geben uns alle relevanten Garantien.
Andere Tipps
Sie können auch diese betrachten aus der Linux Kernel Dokumentation .
C-Programmierer haben oft flüchtig gemacht, dass die Variable bedeuten veränderte außerhalb des aktuellen Ausführungs-Thread könnte; Als ein Dadurch werden sie manchmal zu verwenden, um es im Kernel-Code versucht, wenn gemeinsam genutzten Datenstrukturen verwendet werden. Mit anderen Worten, sie haben zur Behandlung von flüchtigen Typen als eine Art leicht atomaren Variable bekannt, die Sie sind nicht. Die Verwendung von flüchtigen im Kernel-Code ist so gut wie nie richtig; Dieses Dokument beschreibt, warum.
Der entscheidende Punkt im Hinblick auf flüchtige zu verstehen ist, dass seine Ziel ist es, Unterdrückungs-Optimierung, das ist so gut wie nie, was man wirklich tun will. Im Kernel muss man gemeinsam genutzter Daten schützen Strukturen vor unerwünschtem gleichzeitigem Zugriff, was sehr viel ist andere Aufgabe. Das Verfahren zum Schutz gegen ungewollten Parallelität wird auch fast alle Optimierungsbezogene Probleme vermeiden in einer effizienteren Weise.
Wie volatil, die Kernel-Grundelemente, die den gleichzeitigen Zugriff machen Daten sicher (spinlocks, mutexes, Speichersperren usw.) sind so konzipiert, verhindern, dass unerwünschte Optimierung. Wenn sie richtig verwendet werden, da wird nicht nötig sein, auch flüchtig zu verwenden. Wenn flüchtig ist nach wie vor notwendig, ist es fast sicher ein Fehler im Code irgendwo. Im richtig geschriebener Kernel-Code, volatile kann nur dazu dienen, um die Dinge langsam nach unten.
Betrachten wir einen typischen Block von Kernel-Code:
spin_lock(&the_lock); do_something_on(&shared_data); do_something_else_with(&shared_data); spin_unlock(&the_lock);
Wenn der gesamte Code folgt die Sperrregeln, den Wert von shared_data nicht ändern kann unerwartet während the_lock gehalten wird. Jeder andere Code die vielleicht wollen mit, dass die Daten spielen auf die Sperre warten. Die spinlock Primitiven wirken als Speicher Barrieren - sie sind ausdrücklich geschrieben, dies zu tun - was bedeutet, dass Datenzugriffe werden nicht optimiert werden über sie. So dass der Compiler könnte denken, es weiß, was in seine shared_data, aber die spin_lock () -Aufruf, da es fungiert als Speicher Barriere, wird es zwingen, etwas zu vergessen, es weiß. Es wird kein geben Optimierungsprobleme mit Zugriffe auf diese Daten.
Wenn shared_data flüchtig erklärt, so würde die Verriegelung noch sein notwendig. Aber der Compiler würde auch von der Optimierung verhindert werden Zugang zu shared_data in der kritischen Abschnitt, wenn wir wissen, dass niemand sonst kann damit arbeiten. Während die Sperre gehalten wird, shared_data ist nicht flüchtig. Wenn sie mit gemeinsam genutzten Daten zu tun, die richtigen Verriegelung macht flüchtige unnötig -. und potenziell schädliche
Die flüchtige Speicherklasse sollte ursprünglich für Memory-Mapped I / O Register. Im Kernel registriert Zugriffe, sollte auch sein, geschützt durch Schlösser, aber man will auch nicht den Compiler „Optimierung“ Registerzugriffe innerhalb eines kritischen Abschnitt. Aber innerhalb der Kernel, I / O-Speicherzugriffe werden immer durch Accessor getan Funktionen; I / O-Zugriff auf den Speicher direkt über Zeiger wird nicht gerne gesehen auf und funktioniert nicht auf allen Architekturen. Diese Accessoren sind geschrieben unerwünschte Optimierung zu verhindern, so noch einmal flüchtig nicht erforderlich.
Eine andere Situation, wo man versucht sein könnte, flüchtig zu verwenden ist, wenn der Prozessor ist busy-Warte auf den Wert einer Variablen. Das Recht Art und Weise eine busy wait auszuführen ist:
while (my_variable != what_i_want) cpu_relax();
Die cpu_relax () -Aufruf kann CPU Stromverbrauch senken oder zu einer Ausbeute Hyper-Threading-Doppelprozessor; es kommt auch als Speicher dienen Schranke, so noch einmal flüchtig ist nicht erforderlich. Na sicher, Busy-Warte ist in der Regel ein anti-sozialer Akt zu beginnen.
Es gibt noch ein paar seltenen Situationen, in denen flüchtige sinnvoll, der Kernel:
Die oben genannten Zugriffsfunktionen verwenden könnten, volAtile auf Architekturen, bei denen direkte I / O-Speicherzugriff funktioniert. Im Wesentlichen, jedes Accessor Anruf wird ein wenig kritischen Abschnitt auf seiner eigenen und stellt sicher, dass der Zugriff durch den Programmierer wie erwartet geschieht.
Assembler-Code Inline, die Speicher ändert, die aber keine andere sichtbare Nebenwirkungen, Risiken durch GCC gelöscht werden. Hinzufügen des flüchtigen Schlüsselwort asm-Anweisungen wird diese Entfernung verhindern.
Die jiffies Variable ist in besonders, dass es einen anderen Wert haben kann jedesmal, wenn es referenziert wird, aber es kann ohne besondere gelesen werden Verriegelung. So jiffies können flüchtig sein, aber die Zugabe von anderen Variablen dieser Art ist stark verpönt. Jiffies betrachtet ein „dummes Vermächtnis“ -Ausgabe (Linus Worte) in dieser Hinsicht zu sein; es reparieren würde mehr Ärger als es wert ist.
Zeiger auf Datenstrukturen in zusammenhängenden Speicher, die modifiziert werden könnte von I / O-Geräte können manchmal zu Recht volatil sein. Ein Ringpuffer von einem Netzwerkadapter verwendet, wobei der Adapter ändert Zeiger auf zeigen die Deskriptoren verarbeitet wurden, ist ein Beispiel dafür Art von Situation.
Für die meisten Codes, keine der oben genannten Begründungen für volatile gelten. Als Ergebnis und die Verwendung von flüchtigen wahrscheinlich als Fehler gesehen werden zusätzliche Kontrolle, um den Code bringen wird. Entwickler, die sind versucht flüchtig zu verwenden, sollten Sie einen Schritt zurück zu nehmen und darüber nachdenken, was sie versuchen, wirklich zu erreichen.
Ich glaube nicht irrt dich - volatile notwendig ist, dass Thread A zu gewährleisten, wird die Wertänderung sehen, wenn der Wert von etwas anderes als Gewinde A. geändert wird, wie ich es verstehe, flüchtig ist im Grunde eine Art und Weise den Compiler zu sagen, „in einem Register diese Variable nicht cachen, sondern achten sie darauf, immer lesen / schreiben sie es aus dem RAM-Speicher bei jedem Zugriff“.
Die Verwirrung ist da volatile für die Umsetzung eine Reihe von Dingen, nicht ausreichend ist. Insbesondere moderne Systeme verwenden mehrere Ebenen von Caching, moderne Multi-Core-CPUs haben einige Phantasie Optimierungen zur Laufzeit und moderne Compiler tun einige ausgefallene Optimierungen bei der Kompilierung, und diese alle in verschiedenen Nebenwirkungen zeigt sich in einem anderen führen kann um von der Bestellung Sie es erwarten würden, wenn Sie sich den Quellcode nur an.
So flüchtig ist in Ordnung, solange man im Auge behalten, dass die ‚beobachteten‘ Veränderungen in der flüchtigen Variable nicht auf der exakten Zeit auftreten können, Sie denken, sie werden. Insbesondere versuchen Sie nicht flüchtige Variablen als eine Möglichkeit, zu synchronisieren oder um Operationen über Threads zu verwenden, da sie nicht zuverlässig funktionieren.
Persönlich meine main (nur?) Verwendung für den flüchtigen Flag als „pleaseGoAwayNow“ boolean. Wenn ich einen Arbeiter-Thread, die kontinuierlich Schleifen, werde ich es die flüchtige boolean bei jeder Iteration der Schleife überprüfen, und Beenden, wenn die Boolesche immer wahr ist. Der Haupt-Thread kann dann sicher den Arbeitsthread aufzuräumen durch die Booleschen auf true setzen, und dann rufen pthread_join () zu warten, bis der Arbeiter-Thread verschwunden ist.
Ihr Verständnis wirklich falsch ist.
Die Eigenschaft, dass die flüchtigen Variablen haben, wird „liest und schreibt zu dieser Variablen sind Teil der wahrnehmbaren Verhalten des Programms“. Das bedeutet dieses Programm funktioniert (bei entsprechender Hardware):
int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles
Das Problem ist, das ist nicht die Eigenschaft, die wir von Thread-sicher etwas wollen.
Zum Beispiel kann ein Thread-sicheren Zähler wären nur (Linux-Kernel-Code wie, wissen nicht, die C ++ 0x-äquivalent):
atomic_t counter;
...
atomic_inc(&counter);
Dies ist atomar, ohne dass eine Speicherbarriere. Sie sollten sie bei Bedarf hinzufügen. Hinzufügen von flüchtigen würde wahrscheinlich nicht helfen, weil sie nicht den Zugang zu den nahe gelegenen Code beziehen würde (z. B. zum Anhängen eines Elements in die Liste der Zähler Zählung). Sicherlich brauchen Sie nicht, den Zähler zu sehen außerhalb Ihres Programms erhöht und Optimierungen sind nach wie vor wünschenswert, zum Beispiel.
atomic_inc(&counter);
atomic_inc(&counter);
kann immer noch auf
optimiert werdenatomically {
counter+=2;
}
, wenn der Optimierer ist intelligent genug, um (es nicht die Semantik des Codes ändern).
volatile
ist nützlich (wenn auch nicht ausreichend) für das Grundkonstrukt eines spinlock Mutex Umsetzung, aber sobald Sie haben, dass (oder etwas überlegen), brauchen Sie nicht eine andere volatile
.
Der typische Weg von Multithread-Programmierung ist nicht jede gemeinsame Variable auf der Maschinenebene zu schützen, sondern guard Variablen, den Führungsprogrammablauf einzuführen. Statt volatile bool my_shared_flag;
sollten Sie
pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;
Dies hat nicht nur kapselt den „harten Teil,“ es ist grundsätzlich erforderlich: C nicht enthält atomare Operationen notwendig, einen Mutex zu implementieren; es hat nur volatile
um zusätzliche Garantien über normale Operationen zu machen.
Jetzt haben Sie so etwas wie folgt aus:
pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );
pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );
my_shared_flag
muss nicht flüchtig sein, obwohl sie uncachebar, weil
- Ein anderer Thread hat Zugriff darauf.
- Bedeutung ein Verweis auf sie muss irgendwann getroffen wurde (mit dem
&
Operator).- (oder ein Verweis auf eine enthaltende Struktur genommen wurde)
-
pthread_mutex_lock
ist eine Bibliotheksfunktion. - Bedeutung der Compiler kann nicht sagen, ob
pthread_mutex_lock
erwirbt irgendwie diese Referenz. - Bedeutung muss der Compiler übernehmen , die
pthread_mutex_lock
modifes das gemeinsame Flagge - So muss die Variable aus dem Speicher geladen werden.
volatile
, während sinnvoll in diesem Zusammenhang ist fremd.
Für Ihre Daten in einer Umgebung mit gemeinsamer Zugriff, um im Einklang Sie zwei Bedingungen müssen gelten:
1) Atomicity das heißt, wenn ich lesen oder einige Daten in den Speicher schreiben dann diese Daten wird in einem Durchgang gelesen / geschrieben und kann aufgrund unterbrochen oder behauptet werden, beispiels ein Kontextschalter
2) Konsistenz heißt die Reihenfolge der Lese- / Schreib-ops muss gesehen das gleiches zwischen mehreren gleichzeitigen Umgebungen sein -, dass Threads, Maschinen usw.
volatile fits weder der oben -. Oder insbesondere die C oder C ++ Standard, wie volatil verhalten soll schließt weder die oben
Es ist noch schlimmer, in der Praxis als einige Compiler (wie die Intel Itanium-Compiler) versuchen Sie ein Element der gleichzeitigen Zugriff sicheren Verhaltens zu implementieren (dh durch die Speicher Zäune zu gewährleisten) jedoch gibt es keine Konsistenz über Compiler-Implementierungen und darüber hinaus der Standard tut nicht erfordert dies der Umsetzung in erster Linie.
eine Variable als flüchtige Markierung bedeutet nur, dass Sie den Wert zwingen zu und jedes Mal aus dem Speicher geleert werden, die in vielen Fällen nur den Code verlangsamen, wie Sie im Grunde Ihre Cache-Leistung geblasen haben.
C # und Java AFAIK do Abhilfe dies, indem sie flüchtigen haften 1) und 2) aber das gleiche kann nicht für C / C ++ Compiler gesagt werden, so dass im Grunde mit ihm tun, wie Sie für richtig halten.
Für einige mehr in die Tiefe (wenn auch nicht unvoreingenommene) Diskussion über das Thema gelesen dieses
Die comp.programming.threads FAQ hat eine klassische Erklärung von Dave Butenhof:
Q56: Warum kann ich nicht brauchen, um gemeinsam genutzte Variablen VOLATILE zu erklären?
Ich bin jedoch besorgt über Fälle, in denen sowohl der Compiler und die Themen Bibliothek ihre jeweiligen Spezifikationen erfüllen. Ein konformes C-Compiler kann global einige gemeinsam genutzte (nicht-flüchtigen) variable zuzuteilen wie die CPU aus einem Register, das gespeichert und wiederhergestellt wird übergeben wird Faden einzufädeln. Jeder Thread wird seinen eigenen privaten Wert für diese gemeinsame Variable, das ist nicht das, was wir von einem gemeinsamen wollen Variable.
In gewissem Sinne ist dies der Fall, wenn der Compiler genug über die weiß entsprechende Bereiche der Variablen und der pthread_cond_wait (oder pthread_mutex_lock) Funktionen. In der Praxis wird nicht die meisten Compiler versuchen Register Kopien der globalen Daten über einen Anruf zu einem externen zu halten Funktion, weil es zu schwer zu wissen, ob die Routine Macht irgendwie hat Zugriff auf die Adresse der Daten.
Also ja, es ist wahr, dass ein Compiler, dass Konform streng (aber sehr aggressiv) zu ANSI C nicht mit mehreren Threads arbeiten könnten, ohne volatil. Aber jemand hatte besser beheben. Da jedes System (das heißt, pragmatisch, eine Kombination aus kernel, Bibliotheken und C-Compiler) dass man nicht die Speicherkohärenz garantiert POSIX bietet keine CONFORM auf den POSIX-Standard. Zeitraum. Das System kann Sie nicht benötigen verwenden flüchtig auf Umgebungsvariablen für das richtige Verhalten, weil POSIX nur erforderlich, dass die POSIX-Synchronisationsfunktionen notwendig sind.
Also, wenn Ihr Programm bricht, weil Sie nicht flüchtig verwenden, das ein Bug ist. Es kann nicht um einen Fehler in C, oder ein Fehler in der Thread-Bibliothek, oder ein Fehler sein in die Kernel. Aber es ist ein Systemfehler, und ein oder mehr diese Komponenten wird an der Arbeit hat, es zu beheben.
Sie wollen nicht flüchtig verwenden, da auf jedem System, wo es macht jede Differenz, wird es erheblich teurer sein als eine richtige mit nichtflüchtigem variablem. (ANSI C erfordert „-Sequenz Punkte“ für flüchtige Variablen bei jedem Ausdruck, während POSIX verlangt, dass sie nur an Synchronisierungsoperationen - eine rechenintensive Threaded-Anwendung sehen wesentlich mehr Speicheraktivität unter Verwendung von flüchtigen und nach alle, es ist die Speicheraktivität, dass verlangsamt Sie wirklich.)
/ --- [Dave Butenhof] ----------------------- [butenhof@zko.dec.com] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Better Living Through Concurrency] ---------------- /
Herr Butenhof deckt viel von dem gleichen Boden in diese usenet Post :
Die Verwendung von „flüchtig“ ist nicht ausreichend geeigneten Speicher, um sicherzustellen, Sichtbarkeit oder die Synchronisation zwischen Threads. Die Verwendung eines Mutex ist ausreichend, und, mit Ausnahme von verschiedener nicht-tragbarer Maschine zurückgreifen Code Alternativen (oder subtilere Auswirkungen des POSIX-Speicher Regeln, die allgemein gelten viel schwieriger zu, wie erläutert in mein vorheriger Post), ein Mutex notwendig ist.
Daher ist, wie Bryan erklärt, die Verwendung von flüchtigen vollbringt nichts anderes als der Compiler macht nützlich und wünschenswert zu verhindern Optimierungen bei der Herstellung Code „Thread keine Hilfe überhaupt Bereitstellung safe“. Sie sind willkommen, natürlich, alles, was Sie erklären möchten als „Flüchtig“ - es ist ein rechtliches ANSI C Speicher Attribut, nachdem alle. Gerade erwarte nicht, dass es kein Thread-Synchronisation Probleme für Sie zu lösen.
Alles, was gleichermaßen Anwen istKabel an C ++.
Das ist alles, was „flüchtig“ tut: „Hey Compiler, diese Variable kann sich jederzeit ändern (auf jedem CPU-Takt), selbst wenn es keine lokalen WEISE auf sie wirken. Sie diesen Wert nicht in einem Register zwischenzuspeichern.“
Das ist es. Es weist den Compiler an, dass Ihr Wert ist, gut, volatile- Dieser Wert kann (, einen anderen Prozeß einen anderen Thread, die Kernel, etc.) jederzeit durch eine externe Logik verändert werden. Es existiert mehr oder weniger ausschließlich auf Unterdrückungs Compiler-Optimierungen, die geräuschlos einen Wert in einem Register zwischengespeichert werden, dass es zu HAUPT Cache von Natur aus unsicher ist.
Sie können Artikel wie „Dr. Dobbs“ Begegnung, die Tonhöhe flüchtig wie einige Allheilmittel für Multi-Threaded-Programmierung. Sein Ansatz ist nicht völlig frei von Verdienst, aber es hat den grundlegenden Fehler macht ein Objekt Benutzer verantwortlich für die Thread-Sicherheit, die die gleichen Probleme wie andere Verletzungen der Einkapselung haben tendiert.
Nach meinem alten C-Standard, „Was einen Zugriff auf ein Objekt darstellt, das volatile- qualifizierte Art hat, ist die Implementierung definiert“ . So C-Compiler Schriftsteller könnte gewählt hat, um "volatilen" mean hat "sicheren Zugangs in einer Mehrprozessumgebung Thread" . Aber sie tat es nicht.
Stattdessen werden die erforderlichen Operationen einen kritischen Abschnitt Thread-sicher in einem Multi-Core-Multi-Prozess-Shared-Memory-Umgebung machen als neue Implementierung definiert Features hinzugefügt. Und von der Pflicht befreit, dass „flüchtig“ Atom-Zugriff und Zugriffsanordnung in einer Mehrprozessumgebung zur Verfügung stellen würde, der Compiler Schriftsteller priorisiert Code-Reduktion über historische UMSETZUNG abhängige „flüchtig“ Semantik.
Das bedeutet, dass Dinge wie „flüchtig“ Semaphore um kritische Code-Abschnitte, die Arbeit nicht auf neue Hardware mit neuen Compiler, vielleicht einmal mit alten Compiler auf alte Hardware gearbeitet haben, und alte Beispiele sind manchmal nicht falsch, nur alt.