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?

War es hilfreich?

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 werden
atomically {
  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

  1. Ein anderer Thread hat Zugriff darauf.
  2. Bedeutung ein Verweis auf sie muss irgendwann getroffen wurde (mit dem & Operator).
    • (oder ein Verweis auf eine enthaltende Struktur genommen wurde)
  3. pthread_mutex_lock ist eine Bibliotheksfunktion.
  4. Bedeutung der Compiler kann nicht sagen, ob pthread_mutex_lock erwirbt irgendwie diese Referenz.
  5. Bedeutung muss der Compiler übernehmen , die pthread_mutex_lock modifes das gemeinsame Flagge
  6. 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.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top