Warum wird flüchtig in der Multithread-C- oder C ++ - Programmierung nicht als nützlich angesehen?

165

Wie in dieser Antwort, die ich kürzlich gepostet habe, gezeigt wurde, bin ich verwirrt über den Nutzen (oder das Fehlen davon) volatilein 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 foohaben und foovon 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 foosollte 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?

Michael Ekstrand
quelle
7
Alles, was flüchtig ist, ist zu sagen, dass der Compiler den Zugriff auf eine flüchtige Variable nicht zwischenspeichern sollte. Es sagt nichts über die Serialisierung eines solchen Zugriffs aus. Dies wurde hier diskutiert. Ich weiß nicht, wie oft, und ich glaube nicht, dass diese Frage diesen Diskussionen etwas hinzufügen wird.
4
Und wieder wird eine Frage, die es nicht verdient und die hier schon oft gestellt wurde, positiv bewertet. Würdest du bitte damit aufhören?
14
@neil Ich suchte nach anderen Fragen und fand eine, aber jede Erklärung, die ich sah, löste nicht das aus, was ich brauchte, um wirklich zu verstehen, warum ich falsch lag. Diese Frage hat eine solche Antwort hervorgerufen.
Michael Ekstrand
1
Eine ausführliche Studie darüber, was CPUs mit Daten (über ihre Caches) tun, finden Sie unter: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot
1
@curiousguy Das habe ich mit "nicht der Fall in C" gemeint, wo es zum Schreiben in Hardwareregister usw. verwendet werden kann und nicht für Multithreading verwendet wird, wie es üblicherweise in Java verwendet wird.
Monstieur

Antworten:

213

Das Problem volatilein 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 nicht volatile alleine verlassen .

Die Grundelemente, die wir für die verbleibenden Eigenschaften verwenden müssten, bieten jedoch auch diejenigen, die volatiledies tun, sodass dies praktisch nicht erforderlich ist.

Für threadsichere Zugriffe auf gemeinsam genutzte Daten benötigen wir eine Garantie, dass:

  • Das Lesen / Schreiben geschieht tatsächlich (der Compiler speichert den Wert stattdessen nicht nur in einem Register und verschiebt die Aktualisierung des Hauptspeichers bis viel später).
  • dass keine Nachbestellung stattfindet. Angenommen, wir verwenden eine volatileVariable 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 ?

volatilegarantiert den ersten Punkt. Es garantiert auch, dass keine Neuordnung zwischen verschiedenen flüchtigen Lese- / Schreibvorgängen erfolgt . Alle volatileSpeicherzugriffe erfolgen in der angegebenen Reihenfolge. Das ist alles, was wir für das benötigen, wofür volatilevorgesehen ist: das Manipulieren von E / A-Registern oder speicherabgebildeter Hardware, aber es hilft uns nicht bei Multithread-Code, bei dem das volatileObjekt häufig nur zum Synchronisieren des Zugriffs auf nichtflüchtige Daten verwendet wird. Diese Zugriffe können weiterhin relativ zu volatiledenen 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 volatileunnötig wird. Wir können das volatileQualifikationsmerkmal einfach vollständig entfernen .

Seit C ++ 11 std::atomic<T>geben uns atomare Variablen ( ) alle relevanten Garantien.

jalf
quelle
5
@jbcreix: Nach welchem ​​"es" fragst du? Flüchtige oder Gedächtnisbarrieren? In jedem Fall ist die Antwort ziemlich gleich. Beide müssen sowohl auf Compiler- als auch auf CPU-Ebene arbeiten, da sie das beobachtbare Verhalten des Programms beschreiben. Sie müssen also sicherstellen, dass die CPU nicht alles neu ordnet und das von ihnen garantierte Verhalten ändert. Derzeit können Sie jedoch keine tragbare Thread-Synchronisation schreiben, da Speicherbarrieren nicht Teil von Standard-C ++ sind (daher nicht portabel) und volatilenicht stark genug sind, um nützlich zu sein.
Jalf
4
Ein MSDN Beispiel tut dies, und behauptet , dass Befehle nicht an einem flüchtigen Zugang erst nachbestellt werden: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW
27
@OJW: Der Compiler von Microsoft definiert volatilesich 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.
Jalf
4
@Skizz: Nein, hier kommt der Teil der Gleichung "Compiler Magic" ins Spiel. Eine Speicherbarriere muss sowohl von der CPU als auch vom Compiler verstanden werden. Wenn der Compiler die Semantik einer Speicherbarriere versteht, muss er solche Tricks vermeiden (sowie Lese- / Schreibvorgänge über die Barriere hinweg neu anordnen). Und zum Glück, der Compiler tut die Semantik einer Speicherbarriere verstehen, so dass am Ende, es ist alles klappt. :)
Jalf
13
@Skizz: Threads selbst sind vor C ++ 11 und C11 immer eine plattformabhängige Erweiterung. Meines Wissens bietet jede C- und C ++ - Umgebung, die eine Threading-Erweiterung bereitstellt, auch eine "Speicherbarriere" -Erweiterung. Unabhängig davon volatileist für die Multithread-Programmierung immer nutzlos. (Außer unter Visual Studio, wo flüchtige ist die Speicherbarriere - Erweiterung.)
Nemo
49

Sie können dies auch in der Linux-Kernel-Dokumentation berücksichtigen .

C-Programmierer haben häufig als volatil angesehen, dass die Variable außerhalb des aktuellen Ausführungsthreads geändert werden kann. Infolgedessen sind sie manchmal versucht, es im Kernel-Code zu verwenden, wenn gemeinsam genutzte Datenstrukturen verwendet werden. Mit anderen Worten, es ist bekannt, dass sie flüchtige Typen als eine Art einfache atomare Variable behandeln, was sie nicht sind. Die Verwendung von flüchtig im Kernel-Code ist fast nie korrekt. Dieses Dokument beschreibt warum.

Der wichtigste Punkt, den man in Bezug auf Volatilität verstehen muss, ist, dass sein Zweck darin besteht, die Optimierung zu unterdrücken, was fast nie das ist, was man wirklich tun möchte. Im Kernel muss man gemeinsam genutzte Datenstrukturen vor unerwünschtem gleichzeitigen Zugriff schützen, was eine ganz andere Aufgabe ist. Durch den Schutz vor unerwünschter Parallelität werden außerdem fast alle Optimierungsprobleme effizienter vermieden.

Die Kernel-Grundelemente, die den gleichzeitigen Zugriff auf Daten sicher machen (Spinlocks, Mutexe, Speicherbarrieren usw.), sind wie flüchtig, um unerwünschte Optimierungen zu verhindern. Wenn sie ordnungsgemäß verwendet werden, müssen sie auch nicht flüchtig verwendet werden. Wenn immer noch flüchtig ist, gibt es mit ziemlicher Sicherheit irgendwo einen Fehler im Code. In richtig geschriebenem Kernel-Code kann flüchtig nur dazu dienen, die Dinge zu verlangsamen.

Betrachten Sie 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 den Sperrregeln entspricht, kann sich der Wert von shared_data nicht unerwartet ändern, während the_lock gehalten wird. Jeder andere Code, der mit diesen Daten spielen möchte, wartet auf das Schloss. Die Spinlock-Grundelemente fungieren als Speicherbarrieren - sie sind ausdrücklich dafür geschrieben -, was bedeutet, dass Datenzugriffe nicht über sie hinweg optimiert werden. Der Compiler könnte also denken, dass er weiß, was in shared_data enthalten sein wird, aber der Aufruf von spin_lock () zwingt ihn, alles zu vergessen, was er weiß, da er als Speicherbarriere fungiert. Beim Zugriff auf diese Daten treten keine Optimierungsprobleme auf.

Wenn shared_data als flüchtig deklariert würde, wäre die Sperrung weiterhin erforderlich. Der Compiler würde jedoch auch daran gehindert, den Zugriff auf shared_data innerhalb des kritischen Abschnitts zu optimieren , wenn wir wissen, dass niemand anderes damit arbeiten kann. Während die Sperre gehalten wird, sind shared_data nicht flüchtig. Beim Umgang mit gemeinsam genutzten Daten macht eine ordnungsgemäße Sperrung die Flüchtigkeit unnötig - und möglicherweise schädlich.

Die flüchtige Speicherklasse war ursprünglich für speicherabgebildete E / A-Register gedacht. Innerhalb des Kernels sollten auch Registerzugriffe durch Sperren geschützt werden, aber man möchte auch nicht, dass der Compiler die Registerzugriffe innerhalb eines kritischen Abschnitts "optimiert". Innerhalb des Kernels werden E / A-Speicherzugriffe jedoch immer über Zugriffsfunktionen ausgeführt. Der direkte Zugriff auf den E / A-Speicher über Zeiger ist verpönt und funktioniert nicht auf allen Architekturen. Diese Accessoren sind so geschrieben, dass unerwünschte Optimierungen verhindert werden. Daher ist erneut keine Volatilität erforderlich.

Eine andere Situation, in der man versucht sein könnte, flüchtig zu verwenden, besteht darin, dass der Prozessor beschäftigt ist und auf den Wert einer Variablen wartet. Der richtige Weg, um ein geschäftiges Warten durchzuführen, ist:

while (my_variable != what_i_want)
    cpu_relax();

Der Aufruf von cpu_relax () kann den CPU-Stromverbrauch senken oder einem Hyperprozessor-Doppelprozessor nachgeben. Es dient auch als Speicherbarriere, so dass wiederum keine Flüchtigkeit erforderlich ist. Natürlich ist das Warten im Allgemeinen in der Regel zunächst ein unsozialer Akt.

Es gibt immer noch einige seltene Situationen, in denen Volatilität im Kernel Sinn macht:

  • Die oben genannten Accessor-Funktionen können auf Architekturen, auf denen der direkte E / A-Speicherzugriff funktioniert, flüchtig verwendet werden. Im Wesentlichen wird jeder Accessor-Aufruf für sich genommen zu einem kleinen kritischen Abschnitt und stellt sicher, dass der Zugriff wie vom Programmierer erwartet erfolgt.

  • Inline-Assembler-Code, der den Speicher ändert, aber keine anderen sichtbaren Nebenwirkungen hat, kann von GCC gelöscht werden. Durch Hinzufügen des flüchtigen Schlüsselworts zu asm-Anweisungen wird dieses Entfernen verhindert.

  • Die Variable jiffies ist insofern besonders, als sie bei jedem Verweis einen anderen Wert haben kann, aber ohne spezielle Sperre gelesen werden kann. So können Jiffies volatil sein, aber die Hinzufügung anderer Variablen dieses Typs ist stark verpönt. Jiffies wird in dieser Hinsicht als "dummes Vermächtnis" (Linus 'Worte) angesehen; es zu reparieren wäre mehr Mühe als es wert ist.

  • Zeiger auf Datenstrukturen im kohärenten Speicher, die möglicherweise von E / A-Geräten geändert werden, können manchmal rechtmäßig flüchtig sein. Ein von einem Netzwerkadapter verwendeter Ringpuffer, bei dem dieser Adapter die Zeiger ändert, um anzuzeigen, welche Deskriptoren verarbeitet wurden, ist ein Beispiel für diese Art von Situation.

Für den meisten Code gilt keine der oben genannten Begründungen für flüchtig. Infolgedessen wird die Verwendung von volatile wahrscheinlich als Fehler angesehen und bringt zusätzliche Kontrolle in den Code. Entwickler, die versucht sind, volatile zu verwenden, sollten einen Schritt zurücktreten und darüber nachdenken, was sie wirklich erreichen wollen.

Gemeinschaft
quelle
3
@curiousguy: Ja. Siehe auch gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Sebastian Mach
1
Das spin_lock () sieht aus wie ein regulärer Funktionsaufruf. Das Besondere daran ist, dass der Compiler es speziell behandelt, damit der generierte Code jeden Wert von shared_data "vergisst", der vor spin_lock () gelesen und in einem Register gespeichert wurde, sodass der Wert im neu gelesen werden muss do_something_on () nach dem spin_lock ()?
Synkopiert
1
@underscore_d Mein Punkt ist, dass ich anhand des Funktionsnamens spin_lock () nicht erkennen kann, dass es etwas Besonderes tut. Ich weiß nicht, was drin ist. Insbesondere weiß ich nicht, was in der Implementierung enthalten ist, das den Compiler daran hindert, nachfolgende Lesevorgänge zu optimieren.
Synkopiert
1
Syncopated hat einen guten Punkt. Dies bedeutet im Wesentlichen, dass der Programmierer die interne Implementierung dieser "Sonderfunktionen" kennen oder zumindest sehr gut über ihr Verhalten informiert sein sollte. Dies wirft zusätzliche Fragen auf, wie z. B. - Sind diese speziellen Funktionen standardisiert und funktionieren sie garantiert auf allen Architekturen und Compilern gleich? Gibt es eine Liste solcher Funktionen oder gibt es zumindest eine Konvention, Codekommentare zu verwenden, um Entwicklern zu signalisieren, dass die betreffende Funktion den Code vor einer "Optimierung" schützt?
JustAMartin
1
@Tuntable: Eine private Statik kann von jedem Code über einen Zeiger berührt werden. Und seine Adresse wird genommen. Vielleicht kann die Datenflussanalyse beweisen, dass der Zeiger niemals entweicht, aber das ist im Allgemeinen ein sehr schwieriges Problem, das in der Programmgröße superlinear ist. Wenn Sie sicherstellen können, dass keine Aliase vorhanden sind, sollte es in Ordnung sein, den Zugriff über eine Drehsperre zu verschieben. Aber wenn keine Aliase existieren, volatileist das auch sinnlos. In allen Fällen ist das Verhalten "Aufruf einer Funktion, deren Körper nicht sichtbar ist" korrekt.
Ben Voigt
11

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.

Jeremy Friesner
quelle
2
Ihre Boolesche Flagge ist wahrscheinlich unsicher. Wie stellen Sie sicher, dass der Worker seine Aufgabe erfüllt und das Flag bis zum Lesen im Gültigkeitsbereich bleibt (wenn es gelesen wird)? Das ist ein Job für Signale. Volatile eignet sich für die Implementierung einfacher Spinlocks, wenn kein Mutex beteiligt ist, da Aliasicherheit bedeutet, dass der Compiler davon ausgeht mutex_lock(und jede andere Bibliotheksfunktion), den Status der Flag-Variablen zu ändern.
Potatoswatter
6
Offensichtlich funktioniert es nur, wenn die Routine des Worker-Threads so beschaffen ist, dass garantiert wird, dass der Boolesche Wert regelmäßig überprüft wird. Das flüchtige Bool-Flag bleibt garantiert im Gültigkeitsbereich, da die Thread-Shutdown-Sequenz immer vor dem Zerstören des Objekts, das den flüchtigen Booleschen Wert enthält, erfolgt und die Thread-Shutdown-Sequenz nach dem Setzen des Bools pthread_join () aufruft. pthread_join () wird blockiert, bis der Arbeitsthread verschwunden ist. Signale haben ihre eigenen Probleme, insbesondere wenn sie in Verbindung mit Multithreading verwendet werden.
Jeremy Friesner
2
Es ist nicht garantiert, dass der Worker-Thread seine Arbeit abschließt, bevor der Boolesche Wert wahr ist. Tatsächlich befindet er sich mit ziemlicher Sicherheit in der Mitte einer Arbeitseinheit, wenn der Bool auf true gesetzt ist. Es spielt jedoch keine Rolle, wann der Worker-Thread seine Arbeitseinheit beendet, da der Haupt-Thread nichts anderes tun wird, als in pthread_join () zu blockieren, bis der Worker-Thread auf jeden Fall beendet wird. Die Reihenfolge des Herunterfahrens ist also gut geordnet - der flüchtige Bool (und alle anderen gemeinsam genutzten Daten) werden erst freigegeben, nachdem pthread_join () zurückgegeben wurde, und pthread_join () wird erst zurückgegeben, wenn der Worker-Thread entfernt wurde.
Jeremy Friesner
10
@Jeremy, du bist in der Praxis richtig, aber theoretisch könnte es immer noch kaputt gehen. Auf einem Zwei-Kern-System führt ein Kern ständig Ihren Worker-Thread aus. Der andere Kern setzt den Bool auf true. Es gibt jedoch keine Garantie dafür, dass der Kern des Worker-Threads diese Änderung jemals sieht, dh er hört möglicherweise nie auf, obwohl er den Bool wiederholt überprüft. Dieses Verhalten wird von den Speichermodellen c ++ 0x, java und c # zugelassen. In der Praxis würde dies niemals auftreten, da der ausgelastete Thread höchstwahrscheinlich irgendwo eine Speicherbarriere einfügt, wonach die Änderung am Bool angezeigt wird.
Deft_code
4
Nehmen Sie ein POSIX-System, verwenden Sie eine Echtzeit-Planungsrichtlinie 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 ++ volatilenicht 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.
FooF
7

volatileist 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 anderes volatile.

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 haben

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Dies 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 volatilezusätzliche Garantien für den normalen Betrieb geben.

Jetzt hast du so etwas:

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 volatil sein, obwohl es nicht zwischenspeicherbar ist, weil

  1. Ein anderer Thread hat Zugriff darauf.
  2. Das heißt, ein Verweis darauf muss irgendwann (mit dem &Bediener) aufgenommen worden sein.
    • (Oder es wurde auf eine enthaltende Struktur verwiesen)
  3. pthread_mutex_lock ist eine Bibliotheksfunktion.
  4. Das heißt, der Compiler kann nicht sagen, ob er pthread_mutex_lockdiese Referenz irgendwie erhält.
  5. Das heißt, der Compiler muss davon ausgehen, dass pthread_mutex_lockdas Shared Flag geändert wird !
  6. Die Variable muss also aus dem Speicher neu geladen werden. volatile, obwohl in diesem Zusammenhang sinnvoll, ist irrelevant.
Kartoffelklatsche
quelle
6

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):

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, 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):

atomic_t counter;

...
atomic_inc(&counter);

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.

atomic_inc(&counter);
atomic_inc(&counter);

kann noch optimiert werden

atomically {
  counter+=2;
}

Wenn das Optimierungsprogramm intelligent genug ist (es ändert nichts an der Semantik des Codes).

jpalecek
quelle
6

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

Zebrabox
quelle
3
+1 - garantierte Atomizität war ein weiteres Stück von dem, was mir fehlte. Ich ging davon aus, dass das Laden eines int atomar ist, so dass die flüchtige Verhinderung der Neuordnung die vollständige Lösung auf der Leseseite liefert. Ich denke, es ist eine anständige Annahme für die meisten Architekturen, aber es ist keine Garantie.
Michael Ekstrand
Wann sind einzelne Lese- und Schreibvorgänge im Speicher unterbrechbar und nicht atomar? Gibt es einen Vorteil?
Batbrat
5

Die FAQ zu comp.programming.threads enthält eine klassische Erklärung von Dave Butenhof:

F56: Warum muss ich gemeinsam genutzte Variablen nicht als VOLATIL deklarieren?

Ich bin jedoch besorgt über Fälle, in denen sowohl der Compiler als auch die Thread-Bibliothek ihre jeweiligen Spezifikationen erfüllen. Ein konformer C-Compiler kann einem Register global eine gemeinsam genutzte (nichtflüchtige) Variable zuweisen, die gespeichert und wiederhergestellt wird, wenn die CPU von Thread zu Thread übergeben wird. Jeder Thread hat seinen eigenen privaten Wert für diese gemeinsam genutzte Variable, was wir von einer gemeinsam genutzten Variablen nicht erwarten.

In gewissem Sinne ist dies der Fall, wenn der Compiler genug über die jeweiligen Bereiche der Variablen und die Funktionen pthread_cond_wait (oder pthread_mutex_lock) weiß. In der Praxis werden die meisten Compiler nicht versuchen, Registerkopien globaler Daten während eines Aufrufs einer externen Funktion zu speichern, da es zu schwierig ist zu wissen, ob die Routine möglicherweise Zugriff auf die Adresse der Daten hat.

Ja, es stimmt, dass ein Compiler, der streng (aber sehr aggressiv) ANSI C entspricht, möglicherweise nicht mit mehreren Threads ohne Volatile arbeitet. Aber jemand sollte es besser reparieren. Da jedes SYSTEM (dh pragmatisch gesehen eine Kombination aus Kernel, Bibliotheken und C-Compiler), das keine POSIX-Speicherkohärenzgarantien bietet, nicht dem POSIX-Standard entspricht. Zeitraum. Das System kann NICHT verlangen, dass Sie flüchtige Variablen für gemeinsam genutzte Variablen verwenden, um ein korrektes Verhalten zu erzielen, da für POSIX nur die POSIX-Synchronisationsfunktionen erforderlich sind.

Wenn Ihr Programm bricht, weil Sie nicht flüchtig verwendet haben, ist das ein Fehler. Es ist möglicherweise kein Fehler in C oder ein Fehler in der Thread-Bibliothek oder ein Fehler im Kernel. Es handelt sich jedoch um einen SYSTEM-Fehler, und eine oder mehrere dieser Komponenten müssen funktionieren, um ihn zu beheben.

Sie möchten nicht flüchtig verwenden, da es auf jedem System, auf dem es einen Unterschied macht, erheblich teurer ist als eine richtige nichtflüchtige Variable. (ANSI C erfordert "Sequenzpunkte" für flüchtige Variablen bei jedem Ausdruck, während POSIX sie nur bei Synchronisationsvorgängen benötigt - eine rechenintensive Thread-Anwendung sieht bei Verwendung von flüchtigen Variablen wesentlich mehr Speicheraktivität, und schließlich ist es die Speicheraktivität, die verlangsamt dich wirklich.)

/ --- [Dave Butenhof] ----------------------- [[email protected]] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Besseres Leben durch Parallelität] ---------------- /

Herr Butenhof behandelt in diesem Usenet-Beitrag einen Großteil des gleichen Themas :

Die Verwendung von "flüchtig" reicht nicht aus, um eine ordnungsgemäße Speichersichtbarkeit oder Synchronisation zwischen Threads sicherzustellen. Die Verwendung eines Mutex ist ausreichend, und a Mutex ist notwendig.

Wie Bryan erklärte, bewirkt die Verwendung von volatile nichts anderes, als den Compiler daran zu hindern, nützliche und wünschenswerte Optimierungen vorzunehmen, und bietet keinerlei Hilfe, um Code "threadsicher" zu machen. Sie können natürlich alles, was Sie wollen, als "flüchtig" deklarieren - es ist schließlich ein legales ANSI C-Speicherattribut. Erwarten Sie nur nicht, dass es Probleme mit der Thread-Synchronisierung für Sie löst.

All das gilt auch für C ++.

Tony Delroy
quelle
Die Verbindung ist unterbrochen. es scheint nicht mehr auf das hinzuweisen, was Sie zitieren wollten. Ohne den Text ist es eine bedeutungslose Antwort.
JWW
3

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.

Zack Yezek
quelle
3

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.

David
quelle
Die alten Beispiele erforderten, dass das Programm von Qualitätscompilern verarbeitet wird, die für die Programmierung auf niedriger Ebene geeignet sind. Leider haben "moderne" Compiler die Tatsache, dass der Standard nicht verlangt, dass sie "flüchtig" auf nützliche Weise verarbeiten, als Hinweis darauf genommen, dass der Code, der dies erfordern würde, fehlerhaft ist, anstatt zu erkennen, dass der Standard Nein macht Bemühungen, Implementierungen zu verbieten, die konform, aber von so geringer Qualität sind, dass sie nutzlos sind, aber in keiner Weise düstere Compiler mit geringer Qualität, aber Konformität dulden, die populär geworden sind
Supercat
Auf den meisten Plattformen ist es ziemlich einfach zu erkennen, was volatilezu 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 die volatileerforderlichen Arbeiten auszuführen, untergräbt den Zweck eines Standards.
Supercat