Do czego służy słowo kluczowe „volatile”?

130

Przeczytałem kilka artykułów na temat tego volatilesłowa kluczowego, ale nie mogłem znaleźć jego prawidłowego użycia. Czy mógłbyś mi powiedzieć, do czego ma służyć w C # i Javie?

Mircea
źródło
1
Jednym z problemów związanych z niestabilnością jest to, że oznacza więcej niż jedną rzecz. Informacja dla kompilatora, aby nie wykonywać funky optymalizacji, jest dziedzictwem C. Oznacza to również , że przy dostępie należy używać barier pamięciowych. Ale w większości przypadków kosztuje to tylko wydajność i / lub dezorientuje ludzi. : P
AnorZaken

Odpowiedzi:

94

Zarówno dla języka C #, jak i Java, „volatile” mówi kompilatorowi, że wartość zmiennej nigdy nie może być buforowana, ponieważ jej wartość może się zmienić poza zakresem samego programu. Kompilator uniknie wtedy wszelkich optymalizacji, które mogą spowodować problemy, jeśli zmienna zmieni się „poza jego kontrolą”.

Will A
źródło
@Tom - należycie odnotowane, proszę pana - i poprawione.
Will A
11
Wciąż jest o wiele bardziej subtelne.
Tom Hawtin - tackline
1
Źle. Nie zapobiega buforowaniu. Zobacz moją odpowiedź.
doug65536
167

Rozważmy ten przykład:

int i = 5;
System.out.println(i);

Kompilator może to zoptymalizować, aby po prostu wydrukować 5, na przykład:

System.out.println(5);

Jeśli jednak istnieje inny wątek, który może się zmienić i, jest to niewłaściwe zachowanie. Jeśli inny wątek zmieni się ina 6, zoptymalizowana wersja nadal będzie drukować 5.

volatileKluczowe zapobiega takiej optymalizacji i buforowania, jest zatem korzystne, gdy zmienna może być zmienione przez inny gwint.

Sjoerd
źródło
3
Uważam, że optymalizacja nadal byłaby ważna z ioznaczeniem jako volatile. W Javie chodzi o relacje zdarzają się przed .
Tom Hawtin - tackline
Dzięki za wysłanie, więc jakoś niestabilny ma połączenia ze zmiennym blokowaniem?
Mircea
@Mircea: Właśnie o to mi powiedziano, że oznaczanie czegoś jako zmiennego dotyczyło: oznaczenie pola jako niestabilnego wymagałoby wewnętrznego mechanizmu, który pozwoliłby wątkom zobaczyć spójną wartość dla danej zmiennej, ale nie jest to wspomniane w powyższej odpowiedzi ... może ktoś to potwierdzi czy nie? Dzięki
npinti
5
@Sjoerd: Nie jestem pewien, czy rozumiem ten przykład. Jeśli ijest zmienną lokalną, żaden inny wątek i tak nie może jej zmienić. Jeśli jest to pole, kompilator nie może zoptymalizować wywołania, chyba że tak jest final. Nie sądzę, aby kompilator mógł dokonywać optymalizacji w oparciu o założenie, że pole „wygląda”, finalgdy nie jest wyraźnie zadeklarowane jako takie.
poligenelubricants
1
C # i java nie są C ++. To nie jest poprawne. Nie zapobiega buforowaniu i nie zapobiega optymalizacji. Chodzi o semantykę pozyskiwania odczytu i zwalniania magazynu, które są wymagane na słabo uporządkowanych architekturach pamięci. Chodzi o wykonanie spekulacyjne.
doug65536
40

Aby zrozumieć, co zmienna robi ze zmienną, ważne jest, aby zrozumieć, co się dzieje, gdy zmienna nie jest zmienna.

  • Zmienna jest nieulotna

Gdy dwa wątki A i B uzyskują dostęp do zmiennej nieulotnej, każdy wątek zachowa lokalną kopię zmiennej w swojej lokalnej pamięci podręcznej. Wszelkie zmiany dokonane przez wątek A w jego lokalnej pamięci podręcznej nie będą widoczne dla wątku B.

  • Zmienna jest zmienna

Gdy zmienne są deklarowane jako zmienne, zasadniczo oznacza to, że wątki nie powinny buforować takiej zmiennej lub innymi słowy, wątki nie powinny ufać wartościom tych zmiennych, chyba że są one odczytywane bezpośrednio z pamięci głównej.

Kiedy więc uczynić zmienną zmienną?

Gdy masz zmienną, do której ma dostęp wiele wątków i chcesz, aby każdy wątek otrzymał najnowszą zaktualizowaną wartość tej zmiennej, nawet jeśli wartość jest aktualizowana przez jakikolwiek inny wątek / proces / poza programem.

Saurabh Patil
źródło
2
Źle. Nie ma to nic wspólnego z „zapobieganiem buforowaniu”. Chodzi o zmianę kolejności przez kompilator LUB sprzęt procesora poprzez wykonanie spekulacyjne.
doug65536
37

Odczyty zmiennych zmiennych nabrały semantyki . Oznacza to, że jest zagwarantowane, że pamięć odczytana ze zmiennej ulotnej wystąpi przed jakimkolwiek następnym odczytem pamięci. Blokuje kompilator przed zmianą kolejności, a jeśli sprzęt tego wymaga (słabo uporządkowany procesor), użyje specjalnej instrukcji, aby sprzęt opróżnił wszystkie odczyty, które wystąpią po odczycie ulotnym, ale zostały spekulacyjnie uruchomione wcześnie, lub procesor może przede wszystkim zapobiegać ich wczesnemu wystawianiu, zapobiegając pojawianiu się jakiegokolwiek spekulacyjnego obciążenia między wydaniem ładunku a jego wycofaniem.

Zapisy zmiennych zmiennych mają semantykę wydania . Oznacza to, że gwarantuje się, że wszelkie zapisy w pamięci do zmiennej nietrwałej będą opóźnione, dopóki wszystkie poprzednie zapisy w pamięci nie będą widoczne dla innych procesorów.

Rozważmy następujący przykład:

something.foo = new Thing();

Jeśli foojest zmienną składową w klasie, a inne procesory mają dostęp do instancji obiektu, do której odwołuje się something, mogą zobaczyć foozmianę wartości, zanim zapisy do pamięci w Thingkonstruktorze będą widoczne globalnie! To właśnie oznacza „słabo uporządkowana pamięć”. Może to nastąpić nawet wtedy, gdy kompilator ma wszystkie sklepy w konstruktorze przed sklepem foo. Jeśli footak, volatilemagazyn foobędzie miał semantykę wydania, a sprzęt gwarantuje, że wszystkie zapisy przed zapisem foosą widoczne dla innych procesorów przed zezwoleniem na foowystąpienie zapisu .

Jak to możliwe, fooże pisma są tak źle uporządkowane? Jeśli wstrzymanie linii pamięci podręcznej fooznajduje się w pamięci podręcznej, a magazyny w konstruktorze przegapiły pamięć podręczną, wówczas sklep może zakończyć się znacznie wcześniej niż zapisy do pamięci podręcznej chybiają.

(Okropna) architektura Itanium firmy Intel miała słabo uporządkowaną pamięć. Procesor użyty w oryginalnym XBox 360 miał słabo uporządkowaną pamięć. Wiele procesorów ARM, w tym bardzo popularny ARMv7-A, ma słabo uporządkowaną pamięć.

Programiści często nie widzą tych wyścigów danych, ponieważ takie rzeczy jak blokady będą tworzyć pełną barierę pamięci, zasadniczo to samo, co pozyskiwanie i zwalnianie semantyki w tym samym czasie. Żadne obciążenia wewnątrz śluzy nie mogą być spekulatywnie wykonywane przed pozyskaniem zamka, są one opóźniane do momentu nabycia zamka. Żadne zapasy nie mogą być opóźnione przez zwolnienie blokady, instrukcja zwalniająca blokadę jest opóźniona do momentu, gdy wszystkie zapisy wykonane wewnątrz zamka będą widoczne globalnie.

Bardziej kompletnym przykładem jest wzorzec „Podwójnie sprawdzone blokowanie”. Celem tego wzorca jest uniknięcie konieczności uzyskiwania blokady w celu leniwego zainicjowania obiektu.

Zaczepiony z Wikipedii:

public class MySingleton {
    private static object myLock = new object();
    private static volatile MySingleton mySingleton = null;

    private MySingleton() {
    }

    public static MySingleton GetInstance() {
        if (mySingleton == null) { // 1st check
            lock (myLock) {
                if (mySingleton == null) { // 2nd (double) check
                    mySingleton = new MySingleton();
                    // Write-release semantics are implicitly handled by marking
                    // mySingleton with 'volatile', which inserts the necessary memory
                    // barriers between the constructor call and the write to mySingleton.
                    // The barriers created by the lock are not sufficient because
                    // the object is made visible before the lock is released.
                }
            }
        }
        // The barriers created by the lock are not sufficient because not all threads
        // will acquire the lock. A fence for read-acquire semantics is needed between
        // the test of mySingleton (above) and the use of its contents. This fence
        // is automatically inserted because mySingleton is marked as 'volatile'.
        return mySingleton;
    }
}

W tym przykładzie sklepy w MySingletonkonstruktorze mogą nie być widoczne dla innych procesorów przed sklepem mySingleton. Jeśli tak się stanie, inne wątki, które zaglądają do mySingleton, nie otrzymają blokady i niekoniecznie przejmą zapisy do konstruktora.

volatilenigdy nie zapobiega buforowaniu. Gwarantuje porządek, w jakim „widzą” inne procesory. Zwolnienie magazynu opóźni magazyn, dopóki wszystkie oczekujące zapisy nie zostaną zakończone i zostanie wydany cykl magistrali, informujący inne procesory o odrzuceniu / zapisaniu zwrotnym linii pamięci podręcznej, jeśli zdarzy się, że mają buforowane odpowiednie linie. Pozyskiwanie obciążenia usunie spekulowane odczyty, zapewniając, że nie będą one przestarzałymi wartościami z przeszłości.

doug65536
źródło
Dobre wytłumaczenie. Również dobry przykład podwójnego sprawdzenia blokowania. Jednak nadal nie jestem pewien, kiedy użyć, ponieważ martwię się o aspekty buforowania. Jeśli napiszę implementację kolejki, w której tylko 1 wątek będzie zapisywał, a tylko 1 wątek będzie czytał, czy mogę obejść się bez blokad i po prostu oznaczyć moje „wskaźniki” głowy i ogona jako niestabilne? Chcę mieć pewność, że zarówno czytelnik, jak i piszący będą widzieć najbardziej aktualne wartości.
nickdu,
Zarówno headi tailtrzeba być lotny, aby zapobiec producent oparł tailsię nie zmieni, i aby zapobiec konsumenta od zakładając, headnie ulegnie zmianie. Ponadto headmusi być nietrwały, aby zapewnić, że zapisy danych w kolejce są globalnie widoczne, zanim sklep headbędzie widoczny globalnie.
doug65536
+1, Terminy takie jak najnowsze / „najbardziej aktualne” niestety sugerują pojęcie pojedynczej poprawnej wartości. W rzeczywistości dwóch zawodników może przekroczyć linię mety dokładnie w tym samym czasie - na procesorze dwa rdzenie mogą zażądać zapisu dokładnie w tym samym czasie . W końcu rdzenie nie wykonują pracy na zmianę - to sprawiłoby, że wielordzeniowy byłby bezcelowy. Dobre myślenie / projektowanie wielowątkowe nie powinno skupiać się na próbach wymuszania „nowości” niskiego poziomu - z natury fałszywych, ponieważ blokada po prostu wymusza na rdzeniach arbitralny wybór jednego głośnika na raz bez uczciwości - ale raczej spróbuj zaprojektować potrzeba takiej nienaturalnej koncepcji.
AnorZaken
34

Lotny kluczowe ma różne znaczenia zarówno Java i C #.

Jawa

Ze specyfikacji języka Java :

Pole może zostać zadeklarowane jako ulotne, w którym to przypadku model pamięci Java zapewnia, że ​​wszystkie wątki widzą spójną wartość zmiennej.

DO#

Z odwołania C # do volatile słowa kluczowego :

Słowo kluczowe volatile wskazuje, że pole może zostać zmodyfikowane w programie przez coś takiego, jak system operacyjny, sprzęt lub współbieżnie wykonywany wątek.

krock
źródło
Dziękuję bardzo za wysłanie wiadomości, jak zrozumiałem w Javie działa to jak blokowanie tej zmiennej w kontekście wątku, aw C # jeśli jest używana, wartość zmiennej można zmienić nie tylko z programu, czynniki zewnętrzne, takie jak system operacyjny, mogą modyfikować jej wartość ( brak domniemania blokady) ... Proszę dać mi znać, jeśli dobrze zrozumiałem te różnice ...
Mircea
@Mircea w Javie nie ma blokowania, po prostu zapewnia, że ​​zostanie użyta najbardziej aktualna wartość zmiennej lotnej.
krock
Czy Java obiecuje jakąś barierę pamięci, czy może podobnie jak C ++ i C # tylko obiecuje nie optymalizować odniesienia?
Steven Sudit
Bariera pamięci to szczegół implementacji. Java faktycznie obiecuje, że wszystkie odczyty będą miały wartość zapisaną przez najnowszy zapis.
Stephen C
1
@StevenSudit Tak, jeśli sprzęt wymaga bariery lub załadowania / nabycia lub przechowywania / wydania, zastosuje te instrukcje. Zobacz moją odpowiedź.
doug65536
9

W Javie „volatile” jest używane do informowania maszyny JVM, że zmienna może być używana przez wiele wątków w tym samym czasie, więc nie można zastosować pewnych typowych optymalizacji.

W szczególności sytuacja, w której dwa wątki uzyskujące dostęp do tej samej zmiennej działają na oddzielnych procesorach w tej samej maszynie. Procesory bardzo często zapisują w pamięci podręcznej dane, które przechowują, ponieważ dostęp do pamięci jest znacznie wolniejszy niż dostęp do pamięci podręcznej. Oznacza to, że jeśli dane są aktualizowane w CPU1, muszą natychmiast przejść przez wszystkie pamięci podręczne i do pamięci głównej, a nie wtedy, gdy pamięć podręczna zdecyduje się wyczyścić, aby CPU2 mógł zobaczyć zaktualizowaną wartość (ponownie, pomijając wszystkie pamięci podręczne po drodze).

Thorbjørn Ravn Andersen
źródło
1

Podczas odczytywania danych, które są nieulotne, wątek wykonawczy może, ale nie musi, zawsze uzyskać zaktualizowaną wartość. Ale jeśli obiekt jest niestabilny, wątek zawsze otrzymuje najbardziej aktualną wartość.

Subhash Saini
źródło
1
Czy możesz przeformułować swoją odpowiedź?
Anirudha Gupta
volatile da ci najbardziej aktualną wartość zamiast wartości z pamięci podręcznej.
Subhash Saini
0

Zmienna to rozwiązanie problemu współbieżności. Aby zsynchronizować tę wartość. To słowo kluczowe jest najczęściej używane w wątkach. Gdy wiele wątków aktualizuje tę samą zmienną.

Shiv Ratan Kumar
źródło
1
Nie sądzę, żeby to „rozwiązało” problem. To narzędzie, które pomaga w pewnych okolicznościach. Nie polegaj na niestabilności w sytuacjach, w których wymagana jest blokada, na przykład podczas wyścigu.
Scratte