Wann wird das flüchtige Schlüsselwort in C # verwendet?

Die vom JIT-Compiler (Just-in-Time) in der Common Language Runtime verwendeten Optimierungstechniken können zu unvorhersehbaren Ergebnissen führen, wenn Ihr .Net-Programm versucht, nichtflüchtige Datenlesevorgänge in einem Multithread-Szenario durchzuführen. In diesem Artikel werden die Unterschiede zwischen flüchtigem und nichtflüchtigem Speicherzugriff, die Rolle des flüchtigen Schlüsselworts in C # und die Verwendung des flüchtigen Schlüsselworts untersucht.

Ich werde einige Codebeispiele in C # bereitstellen, um die Konzepte zu veranschaulichen. Um zu verstehen, wie das Schlüsselwort volatile funktioniert, müssen wir zunächst verstehen, wie die Optimierungsstrategie des JIT-Compilers in .Net funktioniert.

Grundlegendes zu JIT-Compiler-Optimierungen

Es ist zu beachten, dass der JIT-Compiler im Rahmen einer Optimierungsstrategie die Reihenfolge der Lese- und Schreibvorgänge so ändert, dass die Bedeutung und die eventuelle Ausgabe des Programms nicht geändert werden. Dies ist in dem unten angegebenen Code-Snippet dargestellt.

x = 0;

x = 1;

Der obige Codeausschnitt kann unter Beibehaltung der ursprünglichen Semantik des Programms wie folgt geändert werden.

x = 1;

Der JIT-Compiler kann auch ein Konzept namens "Konstante Weitergabe" anwenden, um den folgenden Code zu optimieren.

x = 1;

y = x;

Das obige Codefragment kann wie folgt geändert werden - wiederum unter Beibehaltung der ursprünglichen Semantik des Programms.

x = 1;

y = 1;

Flüchtiger oder nichtflüchtiger Speicherzugriff

Das Speichermodell moderner Systeme ist ziemlich kompliziert. Sie haben Prozessorregister, verschiedene Ebenen von Caches und Hauptspeicher, die von mehreren Prozessoren gemeinsam genutzt werden. Wenn Ihr Programm ausgeführt wird, kann der Prozessor die Daten zwischenspeichern und dann aus dem Cache auf diese Daten zugreifen, wenn dies vom ausführenden Thread angefordert wird. Aktualisierungen und Lesevorgänge dieser Daten werden möglicherweise für die zwischengespeicherte Version der Daten ausgeführt, während der Hauptspeicher zu einem späteren Zeitpunkt aktualisiert wird. Dieses Modell der Speichernutzung hat Konsequenzen für Multithread-Anwendungen. 

Wenn ein Thread mit den Daten im Cache interagiert und ein zweiter Thread versucht, dieselben Daten gleichzeitig zu lesen, liest der zweite Thread möglicherweise eine veraltete Version der Daten aus dem Hauptspeicher. Dies liegt daran, dass beim Aktualisieren des Werts eines nichtflüchtigen Objekts die Änderung im Cache des ausführenden Threads und nicht im Hauptspeicher vorgenommen wird. Wenn jedoch der Wert eines flüchtigen Objekts aktualisiert wird, wird nicht nur die Änderung im Cache des ausführenden Threads vorgenommen, sondern dieser Cache wird dann in den Hauptspeicher geleert. Wenn der Wert eines flüchtigen Objekts gelesen wird, aktualisiert der Thread seinen Cache und liest den aktualisierten Wert.

Verwenden des flüchtigen Schlüsselworts in C #

Das flüchtige Schlüsselwort in C # wird verwendet, um den JIT-Compiler darüber zu informieren, dass der Wert der Variablen niemals zwischengespeichert werden sollte, da er möglicherweise vom Betriebssystem, der Hardware oder einem gleichzeitig ausgeführten Thread geändert wird. Der Compiler vermeidet daher die Verwendung von Optimierungen für die Variable, die zu Datenkonflikten führen können, dh zu unterschiedlichen Threads, die auf unterschiedliche Werte der Variablen zugreifen.

Wenn Sie ein Objekt oder eine Variable als flüchtig markieren, wird es zu einem Kandidaten für flüchtige Lese- und Schreibvorgänge. Es ist zu beachten, dass in C # alle Speicherschreibvorgänge flüchtig sind, unabhängig davon, ob Sie Daten in ein flüchtiges oder ein nichtflüchtiges Objekt schreiben. Die Mehrdeutigkeit tritt jedoch auf, wenn Sie Daten lesen. Wenn Sie nichtflüchtige Daten lesen, erhält der ausführende Thread möglicherweise immer den neuesten Wert. Wenn das Objekt flüchtig ist, erhält der Thread immer den aktuellsten Wert.

Sie können eine Variable als flüchtig deklarieren, indem Sie ihr das volatileSchlüsselwort voranstellen . Das folgende Codefragment veranschaulicht dies.

Klassenprogramm

    {

        public volatile int i;

        statische Leere Main (string [] args)

        {

            // Schreibe deinen Code hier

        }}

    }}

Sie können das volatileSchlüsselwort für alle Referenz-, Zeiger- und Aufzählungstypen verwenden. Sie können den flüchtigen Modifikator auch mit den Typen Byte, Short, Int, Char, Float und Bool verwenden. Es ist zu beachten, dass lokale Variablen nicht als flüchtig deklariert werden können. Wenn Sie ein Objekt vom Referenztyp als flüchtig angeben, ist nur der Zeiger (eine 32-Bit-Ganzzahl, die auf die Stelle im Speicher verweist, an der das Objekt tatsächlich gespeichert ist) flüchtig, nicht der Wert der Instanz. Außerdem kann eine Doppelvariable nicht flüchtig sein, da sie 64 Bit groß ist und größer als die Wortgröße auf x86-Systemen. Wenn Sie eine Doppelvariable flüchtig machen müssen, sollten Sie sie in die Klasse einschließen. Sie können dies einfach tun, indem Sie eine Wrapper-Klasse erstellen, wie im folgenden Code-Snippet gezeigt.

öffentliche Klasse VolatileDoubleDemo

{

    private volatile WrappedVolatileDouble volatileData;

}}

öffentliche Klasse WrappedVolatileDouble

{

    public double Data {get; einstellen; }}

Beachten Sie jedoch die Einschränkung des obigen Codebeispiels. Obwohl Sie den neuesten Wert des volatileDataReferenzzeigers haben würden, wird Ihnen nicht der neueste Wert der DataEigenschaft garantiert . Die Lösung besteht darin, den WrappedVolatileDoubleTyp unveränderlich zu machen .

Obwohl das flüchtige Schlüsselwort Ihnen in bestimmten Situationen bei der Thread-Sicherheit helfen kann, ist es nicht eine Lösung für alle Probleme mit der Thread-Parallelität. Sie sollten wissen, dass das Markieren einer Variablen oder eines Objekts als flüchtig nicht bedeutet, dass Sie das Schlüsselwort lock nicht verwenden müssen. Das flüchtige Schlüsselwort ist kein Ersatz für das Schlüsselwort lock. Dies dient nur dazu, Datenkonflikte zu vermeiden, wenn mehrere Threads versuchen, auf dieselben Daten zuzugreifen.