Die Referenzzuweisung ist atomar. Warum wird Interlocked.Exchange (ref Object, Object) benötigt?

108

In meinem Multithread-asmx-Webdienst hatte ich ein Klassenfeld _allData meines eigenen Typs SystemData, das aus wenigen besteht List<T>und Dictionary<T>als markiert ist volatile. Die Systemdaten ( _allData) werden von Zeit zu Zeit aktualisiert, und ich erstelle ein anderes Objekt namens newDataund fülle seine Datenstrukturen mit neuen Daten. Wenn es fertig ist, weise ich es einfach zu

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Dies sollte funktionieren, da die Zuweisung atomar ist und die Threads, die auf alte Daten verweisen, diese weiterhin verwenden und der Rest die neuen Systemdaten unmittelbar nach der Zuweisung hat. Mein Kollege sagte jedoch, dass volatileich anstelle von Schlüsselwörtern und einfachen Zuweisungen verwenden sollte, InterLocked.Exchangeda er sagte, dass auf einigen Plattformen nicht garantiert werden kann, dass die Referenzzuweisung atomar ist. Außerdem: wenn ich das the _allDataFeld als volatiledas deklariere

Interlocked.Exchange<SystemData>(ref _allData, newData); 

erzeugt eine Warnung "Ein Verweis auf ein flüchtiges Feld wird nicht als flüchtig behandelt" Was soll ich darüber denken?

Charme
quelle

Antworten:

179

Hier gibt es zahlreiche Fragen. Betrachten Sie sie einzeln:

Die Referenzzuweisung ist atomar. Warum wird Interlocked.Exchange (ref Object, Object) benötigt?

Die Referenzzuordnung ist atomar. Interlocked.Exchange führt nicht nur die Referenzzuweisung durch. Es liest den aktuellen Wert einer Variablen, versteckt den alten Wert und weist der Variablen den neuen Wert als atomare Operation zu.

Mein Kollege sagte, dass auf einigen Plattformen nicht garantiert werden kann, dass die Referenzzuweisung atomar ist. War mein Kollege richtig?

Nein. Die Referenzzuweisung ist auf allen .NET-Plattformen garantiert atomar.

Mein Kollege argumentiert aus falschen Prämissen. Bedeutet das, dass ihre Schlussfolgerungen falsch sind?

Nicht unbedingt. Ihr Kollege könnte Ihnen aus schlechten Gründen gute Ratschläge geben. Vielleicht gibt es einen anderen Grund, warum Sie Interlocked.Exchange verwenden sollten. Lock-free-Programmierung ist wahnsinnig schwierig und sobald Sie von etablierten Praktiken abweichen, die von Experten auf diesem Gebiet vertreten werden, sind Sie im Unkraut und riskieren die schlimmsten Rennbedingungen. Ich bin weder ein Experte auf diesem Gebiet noch ein Experte für Ihren Code, daher kann ich auf die eine oder andere Weise kein Urteil fällen.

erzeugt eine Warnung "Ein Verweis auf ein flüchtiges Feld wird nicht als flüchtig behandelt" Was soll ich darüber denken?

Sie sollten verstehen, warum dies im Allgemeinen ein Problem ist. Dies führt zu einem Verständnis dafür, warum die Warnung in diesem speziellen Fall unwichtig ist.

Der Grund, warum der Compiler diese Warnung ausgibt, liegt darin, dass das Markieren eines Felds als flüchtig bedeutet, dass dieses Feld in mehreren Threads aktualisiert wird. Generieren Sie keinen Code, der die Werte dieses Felds zwischenspeichert, und stellen Sie sicher, dass Lese- oder Schreibvorgänge ausgeführt werden Dieses Feld wird nicht "zeitlich vorwärts und rückwärts verschoben" über Prozessor-Cache-Inkonsistenzen.

(Ich gehe davon aus, dass Sie das alles bereits verstehen. Wenn Sie nicht genau wissen, was flüchtig ist und wie es sich auf die Semantik des Prozessor-Cache auswirkt, verstehen Sie nicht, wie es funktioniert, und sollten keine flüchtigen Programme verwenden. Sperrfreie Programme sind sehr schwer zu finden; stellen Sie sicher, dass Ihr Programm richtig ist, weil Sie verstehen, wie es funktioniert, nicht zufällig.)

Angenommen, Sie erstellen eine Variable, die ein Alias ​​eines flüchtigen Feldes ist, indem Sie einen Verweis an dieses Feld übergeben. Innerhalb der aufgerufenen Methode hat der Compiler überhaupt keinen Grund zu wissen, dass die Referenz eine flüchtige Semantik haben muss! Der Compiler generiert fröhlich Code für die Methode, die die Regeln für flüchtige Felder nicht implementiert, aber die Variable ist ein flüchtiges Feld. Das kann Ihre sperrfreie Logik völlig ruinieren. Die Annahme ist immer, dass auf ein flüchtiges Feld immer mit flüchtiger Semantik zugegriffen wird. Es macht keinen Sinn, es manchmal und manchmal nicht als flüchtig zu behandeln. du musst immer konsistent sein, sonst können Sie keine Konsistenz für andere Zugriffe garantieren.

Daher warnt der Compiler, wenn Sie dies tun, da dies wahrscheinlich Ihre sorgfältig entwickelte sperrfreie Logik völlig durcheinander bringen wird.

Natürlich ist Interlocked.Exchange so geschrieben, dass es ein volatiles Feld erwartet und das Richtige tut. Die Warnung ist daher irreführend. Ich bedauere das sehr; Was wir hätten tun sollen, ist einen Mechanismus zu implementieren, mit dem ein Autor einer Methode wie Interlocked.Exchange der Methode ein Attribut hinzufügen kann, das besagt: "Diese Methode, die einen Ref benötigt, erzwingt eine flüchtige Semantik für die Variable, also unterdrücken Sie die Warnung." Vielleicht werden wir dies in einer zukünftigen Version des Compilers tun.

Eric Lippert
quelle
1
Nach dem, was ich von Interlocked.Exchange gehört habe, wird auch garantiert, dass eine Speicherbarriere erstellt wird. Wenn Sie beispielsweise ein neues Objekt erstellen, dann einige Eigenschaften zuweisen und das Objekt dann in einer anderen Referenz speichern, ohne Interlocked.Exchange zu verwenden, kann der Compiler die Reihenfolge dieser Vorgänge durcheinander bringen, sodass der Zugriff auf die zweite Referenz kein Thread ist. sicher. Ist das wirklich so? Ist es sinnvoll, Interlocked.Exchange zu verwenden?
Mike
12
@ Mike: Wenn es darum geht, was möglicherweise in Low-Lock-Multithread-Situationen beobachtet wird, bin ich genauso unwissend wie der nächste Typ. Die Antwort wird wahrscheinlich von Prozessor zu Prozessor variieren. Sie sollten Ihre Frage an einen Experten richten oder sich über das Thema informieren, wenn es Sie interessiert. Joe Duffys Buch und sein Blog sind gute Ausgangspunkte. Meine Regel: Verwenden Sie kein Multithreading. Verwenden Sie gegebenenfalls unveränderliche Datenstrukturen. Wenn Sie nicht können, verwenden Sie Schlösser. Nur wenn Sie müssen wandelbar Daten ohne Schleusen haben , sollten Sie Low-Lock - Techniken in Betracht ziehen.
Eric Lippert
Danke für deine Antwort Eric. Es interessiert mich in der Tat, deshalb habe ich Bücher und Blogs über Multithreading- und Sperrstrategien gelesen und auch versucht, diese in meinen Code zu implementieren. Aber es gibt noch viel zu lernen ...
Mike
2
@EricLippert Zwischen "Verwenden Sie kein Multithreading" und "Wenn Sie unveränderliche Datenstrukturen verwenden müssen" würde ich die mittlere Zwischenstufe "Lassen Sie einen untergeordneten Thread nur ausschließlich eigene Eingabeobjekte verwenden und der übergeordnete Thread verwendet die Ergebnisse nur wenn das Kind fertig ist ". Wie in var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
John
1
@ John: Das ist eine gute Idee. Ich versuche, Threads wie billige Prozesse zu behandeln: Sie sind dazu da, einen Job zu erledigen und ein Ergebnis zu erzielen, und nicht als zweiter Kontroll-Thread innerhalb der Datenstrukturen des Hauptprogramms herumzulaufen. Aber wenn der Arbeitsaufwand des Threads so groß ist, dass es vernünftig ist, ihn wie einen Prozess zu behandeln, dann sage ich, mach ihn einfach zu einem Prozess!
Eric Lippert
9

Entweder irrt sich Ihr Kollege oder er weiß etwas, was die C # -Sprachenspezifikation nicht weiß.

5.5 Atomizität variabler Referenzen :

"Lese- und Schreibvorgänge der folgenden Datentypen sind atomar: Bool, Char, Byte, Sbyte, Short, Ushort, Uint, Int, Float und Referenztypen."

Sie können also in die flüchtige Referenz schreiben, ohne das Risiko, einen beschädigten Wert zu erhalten.

Sie sollten natürlich vorsichtig sein, wie Sie entscheiden, welcher Thread die neuen Daten abrufen soll, um das Risiko zu minimieren, dass mehr als ein Thread dies gleichzeitig tut.

Guffa
quelle
3
@guffa: ja das habe ich auch gelesen. Dadurch bleibt die ursprüngliche Frage "Die Referenzzuweisung ist atomar. Warum wird Interlocked.Exchange (ref Object, Object) benötigt?" unbeantwortet
char m
@zebrabox: was meinst du? wenn sie nicht sind? was würden Sie tun?
Char m
@matti: Es wird benötigt, wenn Sie einen Wert als atomare Operation lesen und schreiben müssen.
Guffa
Wie oft müssen Sie sich Sorgen machen, dass der Speicher in .NET nicht richtig ausgerichtet ist? Interoplastiges Zeug?
Skurmedel
1
@zebrabox: Die Spezifikation listet diese Einschränkung nicht auf, sondern gibt eine sehr klare Aussage. Haben Sie eine Referenz für eine nicht speicherorientierte Situation, in der das Lesen oder Schreiben einer Referenz nicht atomar ist? Scheint so, als würde dies die sehr klare Sprache in der Spezifikation verletzen.
TJ Crowder
6

Interlocked.Exchange <T>

Setzt eine Variable des angegebenen Typs T auf einen angegebenen Wert und gibt den ursprünglichen Wert als atomare Operation zurück.

Es ändert sich und gibt den ursprünglichen Wert zurück. Es ist nutzlos, weil Sie es nur ändern möchten, und wie Guffa sagte, ist es bereits atomar.

Wenn ein Profiler nicht nachgewiesen hat, dass es sich um einen Engpass in Ihrer Anwendung handelt, sollten Sie in Betracht ziehen, Sperren zu deaktivieren. Es ist einfacher zu verstehen und zu beweisen, dass Ihr Code richtig ist.

Guillaume
quelle
3

Iterlocked.Exchange() ist nicht nur atomar, sondern sorgt auch für die Sichtbarkeit des Gedächtnisses:

Die folgenden Synchronisationsfunktionen verwenden die entsprechenden Barrieren, um die Speicherreihenfolge sicherzustellen:

Funktionen, die kritische Abschnitte betreten oder verlassen

Funktionen, die Synchronisationsobjekte signalisieren

Wartefunktionen

Verriegelte Funktionen

Synchronisations- und Multiprozessorprobleme

Dies bedeutet, dass zusätzlich zur Atomizität Folgendes sichergestellt wird:

  • Für den Thread, der es nennt:
    • Die Anweisungen werden nicht neu angeordnet (vom Compiler, der Laufzeit oder der Hardware).
  • Für alle Themen:
    • Keine Lesevorgänge in den Speicher, die vor dieser Anweisung stattfinden, sehen die Änderung, die diese Anweisung vorgenommen hat.
    • Bei allen Lesevorgängen nach dieser Anweisung wird die durch diese Anweisung vorgenommene Änderung angezeigt.
    • Alle Schreibvorgänge in den Speicher, nachdem dieser Befehl ausgeführt wurde, nachdem diese Befehlsänderung den Hauptspeicher erreicht hat (indem diese Befehlsänderung nach Abschluss in den Hauptspeicher geleert wird und die Hardware ihren eigenen Zeitpunkt nicht spülen darf).
Selalerer
quelle