2016-12-02 5 views
31

Joe Albahari hat eine great series auf Multithreading, das ist ein Muss lesen und sollte auswendig für jeden bekannt sein, der C# Multithreading.Ist das 'volatile' Schlüsselwort in C# immer noch fehlerhaft?

In Teil 4 jedoch erwähnt er die Probleme mit flüchtigem:

Hinweis, dass flüchtige Anwendung nicht daran hindert, eine Schreib gefolgt von einem Lesen aus getauscht werden, und dies kann Rätsel erstellen. Joe Duffy veranschaulicht das Problem gut mit dem folgenden Beispiel: Wenn Test1 und Test2 gleichzeitig auf verschiedenen Threads ausgeführt werden, ist es möglich für a und b beide mit einem Wert von 0 (trotz der Verwendung von flüchtigen auf beide X und y)

mit einem nachgestellten Hinweis, dass die MSDN-Dokumentation falsch ist:

die Dokumentation MSDN besagt, dass die Verwendung des flüchtigen Schlüsselwort stellt sicher, dass der meist up-to-date-Wert in der Gegenwart Feld jederzeit. Das ist falsch, da, wie wir gesehen haben, ein Schreiben gefolgt von einem Lesen neu geordnet werden kann.

Ich habe überprüfte die MSDN documentation, die zuletzt im Jahr 2015 geändert wurde, aber noch aufgeführt:

Das flüchtige Schlüsselwort gibt an, dass ein Feld von mehrere Threads modifiziert werden könnte, die zur gleichen Zeit ausgeführt werden . Felder, die als flüchtig deklariert sind, unterliegen nicht Compiler-Optimierungen, die Zugriff von einem einzelnen Thread annehmen. Dies stellt sicher, dass der aktuellste Wert immer im Feld zu jeder Zeit vorhanden ist.

Im Moment habe ich noch flüchtig vermeiden zugunsten der ausführlicheren Gewinde zu verhindern, veraltete Daten verwenden:

private int foo; 
private object fooLock = new object(); 
public int Foo { 
    get { lock(fooLock) return foo; } 
    set { lock(fooLock) foo = value; } 
} 

Da die Teile über Multithreading im Jahr 2011 geschrieben wurden, ist das Argument heute noch gültig? Sollten Volatilität noch immer zugunsten von Sperren oder vollen Speicherzäunen vermieden werden, um zu verhindern, dass sehr schwer zu erzeugende Fehler auftreten, die, wie erwähnt, sogar vom CPU-Anbieter abhängen, auf dem sie läuft?

+1

Worum geht es in Ihrem Beispiel bei 'return' und Zuweisungsanweisung nach' lock'? – Yarik

+2

Dies ist immer noch irreführend. Volatile bietet Semantiken für das Akquirieren/Freigeben von Speicher, die ausreichen, um viele Algorithmen effizient zu implementieren. Ja, es ist schwierig zu verwenden, aber das macht es nicht kaputt (die Lösung von C++ ist eindeutig überlegen, aber sie hatten den Vorteil, die Probleme mit flüchtigen in Java zu sehen) (Und jeder, der denkt, dass Speicherbarrieren einfacher als flüchtig sind) hat einfach nicht genug Erfahrung mit anderen Architekturen als x86.Versuche, Speicherbarrieren auf einer Architektur ohne Multi-Copy-Atomizität zu verwenden und zu sehen, wie weit du kommst) – Voo

+0

(Die msdn Beschreibung ist zugegebenermaßen merklich schlechter. Offensichtlich versteht derjenige, der dieses Snippet geschrieben hat, überhaupt nicht flüchtig und man kann nur hoffen, dass es nie erlaubt wurde schreibe eine Zeile mit gleichzeitigem Code) – Voo

Antwort

30

Volatile in seiner aktuellen Implementierung ist nicht gebrochen trotz populären Blog-Beiträge, die so etwas behaupten. Es ist jedoch schlecht spezifiziert und die Idee, einen Modifizierer auf einem Feld zu verwenden, um die Speicherordnung zu spezifizieren, ist nicht so toll (vergleichen Sie flüchtig in Java/C# mit der atomaren Spezifikation von C++, die genügend Zeit hatte, aus früheren Fehlern zu lernen). Der MSDN-Artikel auf der anderen Seite wurde eindeutig von jemandem geschrieben, der kein Geschäft über Nebenläufigkeit spricht und völlig falsch ist. Die einzige vernünftige Option ist es, sie vollständig zu ignorieren.

Flüchtige Garantien acquire/release Semantik, wenn das Feld den Zugriff auf und kann nur auf Typen angewendet werden, mit denen Atom liest und schreibt. Nicht mehr und nicht weniger. Dies ist ausreichend, um viele Lock-Free-Algorithmen wie non-blocking hashmaps effizient zu implementieren.

Ein sehr einfaches Beispiel verwendet eine flüchtige Variable, um Daten zu veröffentlichen. Dank der flüchtigen auf x, die Behauptung im folgenden Ausschnitt kann nicht Feuer:

private int a; 
private volatile bool x; 

public void Publish() 
{ 
    a = 1; 
    x = true; 
} 

public void Read() 
{ 
    if (x) 
    { 
     // if we observe x == true, we will always see the preceding write to a 
     Debug.Assert(a == 1); 
    } 
} 

Flüchtige ist nicht einfach zu bedienen und in den meisten Situationen, die Sie viel besser dran mit etwas höheren Level-Konzept zu gehen, aber wenn die Leistung ist wichtig oder Sie implementieren einige Low-Level-Datenstrukturen, volatile kann äußerst nützlich sein.

+2

Haben Sie beabsichtigt, dass "a" flüchtig ist oder volatile auf "x" sicherstellt, dass der Schreibvorgang auf "a" passiert ist? –

+1

@Patrick Der Code ist korrekt wie er ist. Speicherbestellgarantien sind viel strenger als "Schreibvorgänge können nicht zwischengespeichert werden" und dies spielt auch hier eine Rolle. Um es deutlich zu vereinfachen: Wenn Thread B die Aktualisierung der flüchtigen Variablen X sieht, ist es garantiert, alle Schreibvorgänge zu sehen, die zuvor auf Thread A passiert sind, als der Wert auf x geschrieben wurde. Dies ermöglicht es uns, einen einzelnen flüchtigen Bool zu verwenden, um andere Daten zu veröffentlichen. – Voo

+0

Hat Volatile wirklich nur eine Semantik zum Erwerb/zur Freigabe? Oder sequentielle Konsistenz? Das hätte ich mir gedacht, und die beiden sind nicht gleich. – Mehrdad

13

Wenn ich die MSDN-Dokumentation lese, glaube ich, dass Sie sagen, wenn Sie auf eine Variable flüchtig sehen, müssen Sie sich keine Sorgen über Compiler-Optimierungen machen, da sie die Operationen neu anordnen. Es bedeutet nicht, dass Sie vor Fehlern geschützt sind, die dadurch verursacht werden, dass Ihr eigener Code Operationen in separaten Threads in der falschen Reihenfolge ausführt. (obwohl zugegebenermaßen der Kommentar nicht klar ist.)

+6

Ich stimme zu. Ich denke, die Betonung sollte sein "Felder, die als flüchtig deklariert werden, unterliegen nicht ** Compiler-Optimierungen **, die den Zugriff durch einen einzigen Thread voraussetzen. Dies stellt sicher, dass der aktuellste Wert immer im Feld vorhanden ist. " –

+3

@ MikeSherrill'CatRecall ': "Compiler" ist ein Ablenkungsmanöver. Thread-Sicherheit ist ein Problem, das weit über den Compiler hinausreicht. Ähnlich schlecht ist beispielsweise die Nachordnung in der CPU. – Mehrdad

+2

Jeder Compiler muss CPU-Neuordnungsregeln verstehen. Wenn die Befehle A und B von der CPU neu angeordnet werden können, aber die Sprachsemantik etwas anderes vorschreibt, muss der Compiler ** eine Art Zaun implementieren, der die Neuordnung verhindert, oder alternative Anweisungen wählen, um dasselbe Ziel zu erreichen. Daher ist es, wo "flüchtige" Semantiken eine Neusortierung voraussetzen, Sache des Compilers, nicht nur die Neuordnung selbst zu vermeiden, sondern auch die CPU daran zu hindern, dies zu tun. – MSalters

3

volatile ist eine sehr begrenzte Garantie. Dies bedeutet, dass die Variable keinen Compiler-Optimierungen unterliegt, die den Zugriff von einem einzelnen Thread aus annehmen. Das heißt, wenn Sie in eine Variable aus einem Thread schreiben, dann lesen Sie es aus einem anderen Thread, wird der andere Thread definitiv den neuesten Wert haben. Ohne Flüchtigkeit, ein Multiprozessor ohne Flüchtigkeit, kann der Compiler Annahmen über den Singlethread-Zugriff treffen, indem er beispielsweise den Wert in einem Register hält, was verhindert, dass andere Prozessoren auf den neuesten Wert zugreifen können.

Wie das von Ihnen erwähnte Codebeispiel zeigt, schützt es Sie nicht davor, dass Methoden in verschiedenen Blöcken neu angeordnet werden. In der Tat volatile macht jede einzelnen Zugriff auf eine volatile Variable Atom. Es gibt keine Garantien hinsichtlich der Atomizität von Gruppen solcher Zugriffe.

Wenn Sie nur sicherstellen möchten, dass Ihre Immobilie einen aktuellen Einzelwert hat, sollten Sie einfach volatile verwenden können.

Das Problem tritt auf, wenn Sie versuchen, mehrere parallele Vorgänge so auszuführen, als wären sie atomar. Wenn Sie mehrere Operationen als atomare Operationen erzwingen müssen, müssen Sie die gesamte Operation sperren. Betrachten Sie das Beispiel wieder, aber unter Verwendung von Sperren:

class DoLocksReallySaveYouHere 
{ 
    int x, y; 
    object xlock = new object(), ylock = new object(); 

    void Test1()  // Executed on one thread 
    { 
    lock(xlock) {x = 1;} 
    lock(ylock) {int a = y;} 
    ... 
    } 

    void Test2()  // Executed on another thread 
    { 
    lock(ylock) {y = 1;} 
    lock(xlock) {int b = x;} 
    ... 
    } 
} 

Die Sperren verursachen kann einige Synchronisation verursachen, die beidea und b von mit dem Wert verhindern kann 0 (ich habe nicht getestet). Da jedoch sowohl x und y unabhängig gesperrt werden, entweder a oder b kann immer noch nicht-deterministisch mit einem Wert von 0.

So im Fall von Einwickeln der Modifikation einer einzelnen Variablen am Ende, sollen Sie sicher sein, mit volatile, und wäre nicht wirklich sicherer mit lock. Wenn Sie mehrere Operationen atomar ausführen müssen, müssen Sie einen lock um den gesamten atomaren Block herum verwenden, da andernfalls die Zeitplanung immer noch zu nicht-deterministischem Verhalten führt.

+1

"Es bedeutet im Grunde, dass die Variable nicht zwischengespeichert wird" ... Seufzer und eine andere Person, die diesen Mythos verewigt :(Nein, das ist absolut nicht, was flüchtige Garantien und in der Tat volatile Felder auf x86 und anderen Architekturen perfekt im Cache gespeichert werden. Volatile beschränkt auch absolut die Art und Weise, wie Speicherzugriffe neu geordnet werden können, was wesentlich ist, um es nützlich zu machen.Obwohl es eine vernünftige Definition von "nicht im Cache" gab (ist es nicht), wäre dies für absolut jeden Algorithmus völlig nutzlos. – Voo

+0

@Voo Ich sage nur, was mir beigebracht wurde, tut mir leid. Ist das richtiger ?: "Das bedeutet, dass die Variable keinen Compiler-Optimierungen unterliegt, die Zugriff von einem einzelnen Thread annehmen. [...] Ohne flüchtige, ein Multiprozessor-Maschine ohne flüchtige kann der Compiler Annahmen über Singlethread-Zugriffe treffen, indem er beispielsweise den Wert in einem Register belässt, was verhindert, dass andere Prozessoren Zugriff auf den letzten Wert haben. " – zstewart

+1

Das ist richtig, aber vermisst die wesentlichen Teile dessen, was flüchtig ist. Flüchtige Garantien erwerben Semantiken für Lese- und Schreibvorgänge. Zum Beispiel können Sie einen Schreibvorgang * nach * einem flüchtigen Schreibvorgang nicht neu anordnen (Sie können ihn jedoch vor einem flüchtigen Schreibvorgang neu anordnen). Wenn Sie anfangen möchten, das Ganze zu verstehen, könnten Sie zum Beispiel mit [this] beginnen (https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/). Es geht um das JMM, aber das CLR MM ist in der Praxis ziemlich ähnlich. – Voo

Verwandte Themen