Kann jemand eine gute Erklärung für das flüchtige Schlüsselwort in C # geben? Welche Probleme löst es und welche nicht? In welchen Fällen erspart es mir die Verwendung von Sperren?
c#
multithreading
Doron Yaacoby
quelle
quelle
Antworten:
Ich glaube nicht, dass es eine bessere Person gibt, um dies zu beantworten als Eric Lippert (Hervorhebung im Original):
Weitere Informationen finden Sie unter:
quelle
volatile
werden aufgrund der Sperre vorhanden seinWenn Sie etwas mehr über die Funktionsweise des flüchtigen Schlüsselworts erfahren möchten, ziehen Sie das folgende Programm in Betracht (ich verwende DevStudio 2005):
Unter Verwendung der standardmäßig optimierten (Release-) Compilereinstellungen erstellt der Compiler den folgenden Assembler (IA32):
Mit Blick auf die Ausgabe hat der Compiler beschlossen, das ecx-Register zum Speichern des Werts der Variablen j zu verwenden. Für die nichtflüchtige Schleife (die erste) hat der Compiler i dem eax-Register zugewiesen. Ziemliech direkt. Es gibt jedoch ein paar interessante Bits - der Befehl lea ebx, [ebx] ist effektiv ein Multibyte-NOP-Befehl, so dass die Schleife zu einer 16-Byte-ausgerichteten Speicheradresse springt. Das andere ist die Verwendung von edx zum Inkrementieren des Schleifenzählers anstelle eines inc eax-Befehls. Der Befehl add reg, reg hat auf einigen IA32-Kernen eine geringere Latenz als der Befehl inc reg, jedoch nie eine höhere Latenz.
Nun zur Schleife mit dem flüchtigen Schleifenzähler. Der Zähler wird bei [esp] gespeichert und das flüchtige Schlüsselwort teilt dem Compiler mit, dass der Wert immer aus dem Speicher gelesen / in den Speicher geschrieben und niemals einem Register zugewiesen werden soll. Der Compiler geht sogar so weit, beim Aktualisieren des Zählerwerts kein Laden / Inkrementieren / Speichern in drei verschiedenen Schritten (Laden von eax, inc eax, Speichern von eax) durchzuführen, sondern der Speicher wird direkt in einer einzelnen Anweisung (einem Add Mem) geändert , reg). Die Art und Weise, wie der Code erstellt wurde, stellt sicher, dass der Wert des Schleifenzählers im Kontext eines einzelnen CPU-Kerns immer aktuell ist. Keine Operation an den Daten kann zu Beschädigung oder Datenverlust führen (daher wird das Laden / Inc / Store nicht verwendet, da sich der Wert während des Inc ändern kann und somit im Store verloren geht). Da Interrupts erst nach Abschluss des aktuellen Befehls bedient werden können,
Sobald Sie eine zweite CPU in das System einführen, schützt das flüchtige Schlüsselwort nicht davor, dass die Daten gleichzeitig von einer anderen CPU aktualisiert werden. Im obigen Beispiel müssten die Daten nicht ausgerichtet sein, um eine mögliche Beschädigung zu erhalten. Das flüchtige Schlüsselwort verhindert keine mögliche Beschädigung, wenn die Daten nicht atomar verarbeitet werden können. Wenn der Schleifenzähler beispielsweise vom Typ long long (64 Bit) ist, sind zwei 32-Bit-Operationen erforderlich, um den Wert in der Mitte zu aktualisieren was ein Interrupt auftreten kann und die Daten ändern.
Das flüchtige Schlüsselwort eignet sich also nur für ausgerichtete Daten, die kleiner oder gleich der Größe der nativen Register sind, sodass Operationen immer atomar sind.
Das flüchtige Schlüsselwort wurde für die Verwendung bei E / A-Operationen konzipiert, bei denen sich die E / A ständig ändern, aber eine konstante Adresse haben, z. B. ein UART-Gerät mit Speicherzuordnung, und der Compiler sollte den ersten von der Adresse gelesenen Wert nicht weiter verwenden.
Wenn Sie große Datenmengen verarbeiten oder über mehrere CPUs verfügen, benötigen Sie ein Sperrsystem auf höherer Ebene (OS), um den Datenzugriff ordnungsgemäß zu handhaben.
quelle
Wenn Sie .NET 1.1 verwenden, wird das Schlüsselwort volatile benötigt, wenn Sie das Sperren doppelt überprüfen. Warum? Denn vor .NET 2.0 kann das folgende Szenario dazu führen, dass ein zweiter Thread auf ein nicht null, jedoch nicht vollständig erstelltes Objekt zugreift:
Vor .NET 2.0 konnte this.foo die neue Instanz von Foo zugewiesen werden, bevor der Konstruktor ausgeführt wurde. In diesem Fall könnte ein zweiter Thread (während des Aufrufs von Thread 1 an den Konstruktor von Foo) eingehen und Folgendes erfahren:
Vor .NET 2.0 konnten Sie this.foo als flüchtig deklarieren, um dieses Problem zu umgehen. Seit .NET 2.0 müssen Sie das flüchtige Schlüsselwort nicht mehr verwenden, um eine doppelt überprüfte Sperrung durchzuführen.
Wikipedia hat tatsächlich einen guten Artikel über Double Checked Locking und geht kurz auf dieses Thema ein: http://en.wikipedia.org/wiki/Double-checked_locking
quelle
foo
? Ist Thread 1 nichtthis.bar
gesperrt und daher kann nur Thread 1 foo zu einem bestimmten Zeitpunkt initialisieren? Ich meine, Sie überprüfen den Wert, nachdem die Sperre wiederManchmal optimiert der Compiler ein Feld und verwendet ein Register, um es zu speichern. Wenn Thread 1 in das Feld schreibt und ein anderer Thread darauf zugreift, da der Update in einem Register (und nicht im Speicher) gespeichert wurde, erhält der 2. Thread veraltete Daten.
Sie können sich das flüchtige Schlüsselwort so vorstellen, dass es dem Compiler sagt: "Ich möchte, dass Sie diesen Wert im Speicher speichern." Dies garantiert, dass der 2. Thread den neuesten Wert abruft.
quelle
Von MSDN : Der flüchtige Modifikator wird normalerweise für ein Feld verwendet, auf das mehrere Threads zugreifen, ohne die lock-Anweisung zum Serialisieren des Zugriffs zu verwenden. Durch die Verwendung des flüchtigen Modifikators wird sichergestellt, dass ein Thread den aktuellsten Wert abruft, der von einem anderen Thread geschrieben wurde.
quelle
Die CLR optimiert gerne Anweisungen. Wenn Sie also auf ein Feld im Code zugreifen, greift sie möglicherweise nicht immer auf den aktuellen Wert des Felds zu (möglicherweise vom Stapel usw.). Durch Markieren eines Feldes als wird
volatile
sichergestellt, dass der Befehl auf den aktuellen Wert des Feldes zugreift. Dies ist nützlich, wenn der Wert (in einem nicht sperrenden Szenario) durch einen gleichzeitigen Thread in Ihrem Programm oder einen anderen im Betriebssystem ausgeführten Code geändert werden kann.Sie verlieren offensichtlich einige Optimierungen, aber es hält den Code einfacher.
quelle
Ich fand diesen Artikel von Joydip Kanjilal sehr hilfreich!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
Ich lasse es einfach hier als Referenz
quelle
Der Compiler ändert manchmal die Reihenfolge der Anweisungen im Code, um sie zu optimieren. Normalerweise ist dies in einer Umgebung mit einem Thread kein Problem, in einer Umgebung mit mehreren Threads kann dies jedoch ein Problem sein. Siehe folgendes Beispiel:
Wenn Sie t1 und t2 ausführen, erwarten Sie keine Ausgabe oder "Wert: 10" als Ergebnis. Es kann sein, dass der Compiler die Zeile innerhalb der Funktion t1 wechselt. Wenn t2 dann ausgeführt wird, könnte es sein, dass _flag den Wert 5 hat, aber _value den Wert 0. Die erwartete Logik könnte also unterbrochen werden.
Um dies zu beheben, können Sie ein flüchtiges Schlüsselwort verwenden, das Sie auf das Feld anwenden können. Diese Anweisung deaktiviert die Compiler-Optimierungen, sodass Sie die richtige Reihenfolge in Ihrem Code erzwingen können.
Sie sollten volatile nur verwenden, wenn Sie es wirklich benötigen, da es bestimmte Compiler-Optimierungen deaktiviert und die Leistung beeinträchtigt. Es wird auch nicht von allen .NET-Sprachen unterstützt (Visual Basic unterstützt es nicht), sodass die Sprachinteroperabilität beeinträchtigt wird.
quelle
Zusammenfassend lässt sich sagen, dass die richtige Antwort auf die Frage lautet: Wenn Ihr Code zur Laufzeit 2.0 oder höher ausgeführt wird, wird das flüchtige Schlüsselwort fast nie benötigt und schadet mehr als es nützt, wenn es unnötig verwendet wird. IE Verwenden Sie es nie. ABER in früheren Versionen der Laufzeit wird es für eine ordnungsgemäße Überprüfung der statischen Felder benötigt. Insbesondere statische Felder, deren Klasse über einen statischen Klasseninitialisierungscode verfügt.
quelle
Mehrere Threads können auf eine Variable zugreifen. Das neueste Update wird für die Variable sein
quelle