Kiedy używać słowa kluczowego volatile w języku C #

Techniki optymalizacji używane przez kompilator JIT (just-in-time) w środowisku wykonawczym języka wspólnego mogą prowadzić do nieprzewidywalnych rezultatów, gdy program .Net próbuje wykonać nieulotne odczyty danych w scenariuszu wielowątkowym. W tym artykule przyjrzymy się różnicom między dostępem do pamięci ulotnej i nieulotnej, roli słowa kluczowego volatile w języku C # oraz sposobowi używania słowa kluczowego volatile.

Podam kilka przykładów kodu w C #, aby zilustrować koncepcje. Aby zrozumieć, jak działa słowo kluczowe volatile, najpierw musimy zrozumieć, jak strategia optymalizacji kompilatora JIT działa w .Net.

Zrozumienie optymalizacji kompilatora JIT

Należy zauważyć, że kompilator JIT w ramach strategii optymalizacji zmieni kolejność odczytów i zapisów w sposób, który nie zmieni znaczenia i ostatecznego wyniku programu. Ilustruje to poniższy fragment kodu.

x = 0;

x = 1;

Powyższy fragment kodu można zmienić na następujący - zachowując oryginalną semantykę programu.

x = 1;

Kompilator JIT może również zastosować koncepcję zwaną „ciągłą propagacją”, aby zoptymalizować następujący kod.

x = 1;

y = x;

Powyższy fragment kodu można zmienić na następujący - ponownie, zachowując oryginalną semantykę programu.

x = 1;

y = 1;

Dostęp do pamięci ulotnej i nieulotnej

Model pamięci współczesnych systemów jest dość skomplikowany. Masz rejestry procesorów, różne poziomy pamięci podręcznych i pamięć główną współdzieloną przez wiele procesorów. Gdy program jest wykonywany, procesor może buforować dane, a następnie uzyskiwać dostęp do tych danych z pamięci podręcznej, gdy zażąda tego wątek wykonawczy. Aktualizacje i odczyty tych danych mogą być wykonywane w odniesieniu do wersji danych z pamięci podręcznej, podczas gdy pamięć główna jest aktualizowana w późniejszym czasie. Ten model wykorzystania pamięci ma konsekwencje dla aplikacji wielowątkowych. 

Kiedy jeden wątek współdziała z danymi w pamięci podręcznej, a drugi wątek próbuje odczytać te same dane jednocześnie, drugi wątek może odczytać nieaktualną wersję danych z pamięci głównej. Dzieje się tak, ponieważ po zaktualizowaniu wartości nieulotnego obiektu zmiana jest dokonywana w pamięci podręcznej wątku wykonawczego, a nie w pamięci głównej. Jednak gdy wartość obiektu ulotnego jest aktualizowana, zmiana dokonywana jest nie tylko w pamięci podręcznej wątku wykonawczego, ale jest ona następnie opróżniana do pamięci głównej. A gdy odczytywana jest wartość obiektu ulotnego, wątek odświeża swoją pamięć podręczną i odczytuje zaktualizowaną wartość.

Używanie słowa kluczowego volatile w C #

Słowo kluczowe volatile w języku C # służy do informowania kompilatora JIT, że wartość zmiennej nigdy nie powinna być buforowana, ponieważ może zostać zmieniona przez system operacyjny, sprzęt lub współbieżnie wykonywany wątek. Kompilator unika w ten sposób wszelkich optymalizacji zmiennej, które mogą prowadzić do konfliktów danych, tj. Do różnych wątków uzyskujących dostęp do różnych wartości zmiennej.

Gdy oznaczysz obiekt lub zmienną jako ulotną, staje się ona kandydatem do nietrwałych odczytów i zapisów. Należy zauważyć, że w C # wszystkie zapisy w pamięci są nietrwałe, niezależnie od tego, czy zapisujesz dane do obiektu ulotnego, czy nieulotnego. Jednak niejednoznaczność pojawia się, gdy czytasz dane. Podczas odczytywania danych, które są nieulotne, wątek wykonawczy może, ale nie zawsze, uzyskać najnowszą wartość. Jeśli obiekt jest ulotny, wątek zawsze pobiera najbardziej aktualną wartość.

Możesz zadeklarować zmienną jako zmienną, poprzedzając ją volatilesłowem kluczowym. Ilustruje to poniższy fragment kodu.

Program zajęć

    {

        publiczne zmienne int i;

        static void Main (string [] args)

        {

            // Wpisz tutaj swój kod

        }

    }

Tego volatilesłowa kluczowego można używać z dowolnymi typami odwołań, wskaźników i wyliczeń. Możesz również użyć modyfikatora volatile z typami bajt, short, int, char, float i bool. Należy zauważyć, że zmiennych lokalnych nie można zadeklarować jako nietrwałych. Po określeniu obiektu typu referencyjnego jako nietrwałego, tylko wskaźnik (32-bitowa liczba całkowita wskazująca lokalizację w pamięci, w której obiekt jest faktycznie przechowywany) jest nietrwały, a nie wartość wystąpienia. Ponadto zmienna double nie może być ulotna, ponieważ ma rozmiar 64 bity, czyli większy niż rozmiar słowa w systemach x86. Jeśli potrzebujesz uczynić zmienną double volatile, powinieneś umieścić ją wewnątrz klasy. Możesz to łatwo zrobić, tworząc klasę opakowania, jak pokazano w poniższym fragmencie kodu.

publiczna klasa VolatileDoubleDemo

{

    private volatile WrappedVolatileDouble volatileData;

}

publiczna klasa WrappedVolatileDouble

{

    public double Data {get; zestaw; }

Należy jednak zwrócić uwagę na ograniczenie powyższego przykładu kodu. Mimo że miałbyś najnowszą wartość volatileDatawskaźnika referencyjnego, nie masz gwarancji, że najnowsza wartość Datawłaściwości. Aby obejść ten problem, należy uczynić WrappedVolatileDoubletyp niezmiennym.

Chociaż słowo kluczowe volatile może pomóc w zabezpieczeniu wątków w niektórych sytuacjach, nie jest rozwiązaniem wszystkich problemów ze współbieżnością wątków. Powinieneś wiedzieć, że oznaczenie zmiennej lub obiektu jako ulotnego nie oznacza, że ​​nie musisz używać słowa kluczowego lock. Słowo kluczowe volatile nie zastępuje słowa kluczowego lock. Ma tylko pomóc w uniknięciu konfliktów danych, gdy masz wiele wątków próbujących uzyskać dostęp do tych samych danych.