przypisanie odniesienia jest niepodzielne, więc dlaczego potrzebne jest Interlocked.Exchange (ref Object, Object)?

108

W mojej wielowątkowej usłudze internetowej asmx miałem pole klasy _allData mojego własnego typu SystemData, które składa się z kilku List<T>i jest Dictionary<T>oznaczone jako volatile. Dane systemowe ( _allData) są od czasu do czasu odświeżane i robię to, tworząc kolejny obiekt o nazwie newDatai wypełniając jego struktury danych nowymi danymi. Kiedy to się skończy, po prostu przypisuję

private static volatile SystemData _allData

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

Powinno to działać, ponieważ przypisanie jest niepodzielne i wątki, które mają odniesienie do starych danych, nadal go używają, a reszta ma nowe dane systemowe zaraz po przypisaniu. Jednak mój kolega powiedział, że zamiast używać volatilesłowa kluczowego i prostego przypisania, powinienem użyć, InterLocked.Exchangeponieważ powiedział, że na niektórych platformach nie ma gwarancji, że przypisanie referencji jest atomowe. Co więcej: jeśli Oświadczam the _allDataboiskavolatile

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

generuje ostrzeżenie „odniesienie do zmiennego pola nie będzie traktowane jako zmienne”. Co mam o tym myśleć?

char m
źródło

Odpowiedzi:

180

Jest tu wiele pytań. Rozważając je pojedynczo:

przypisanie odniesienia jest niepodzielne, więc dlaczego potrzebne jest Interlocked.Exchange (ref Object, Object)?

Przypisanie odniesienia jest atomowe. Interlocked.Exchange nie tylko odnosi się do przypisania. Odczytuje bieżącą wartość zmiennej, usuwa starą wartość i przypisuje nową wartość do zmiennej, wszystko jako operacja niepodzielna.

mój kolega powiedział, że na niektórych platformach nie ma gwarancji, że przypisanie referencji jest atomowe. Czy mój kolega miał rację?

Nie. Przypisanie referencji jest gwarantowane na wszystkich platformach .NET.

Mój kolega wywodzi się z fałszywych przesłanek. Czy to oznacza, że ​​ich wnioski są błędne?

Niekoniecznie. Twój kolega może udzielać ci dobrych rad z niewłaściwych powodów. Być może jest jakiś inny powód, dla którego powinieneś używać Interlocked.Exchange. Programowanie bez blokad jest niesamowicie trudne, a w momencie, gdy odejdziesz od ugruntowanych praktyk popieranych przez ekspertów w tej dziedzinie, jesteś w chwastach i ryzykujesz najgorszymi warunkami wyścigu. Nie jestem ani ekspertem w tej dziedzinie, ani znawcą twojego kodu, więc nie mogę dokonać oceny w ten czy inny sposób.

generuje ostrzeżenie „odniesienie do zmiennego pola nie będzie traktowane jako zmienne”. Co mam o tym myśleć?

Powinieneś zrozumieć, dlaczego jest to ogólnie problem. Pozwoli to zrozumieć, dlaczego ostrzeżenie nie jest ważne w tym konkretnym przypadku.

Kompilator wyświetla to ostrzeżenie, ponieważ oznaczenie pola jako nietrwałego oznacza, że ​​„to pole będzie aktualizowane w wielu wątkach - nie generuj żadnego kodu, który buforuje wartości tego pola i upewnij się, że każdy odczyt lub zapis to pole nie jest „przesuwane do przodu i do tyłu w czasie” przez niespójności pamięci podręcznej procesora. ”

(Zakładam, że już to wszystko rozumiesz. Jeśli nie masz szczegółowego zrozumienia znaczenia zmiennej „ulotna” i jej wpływu na semantykę pamięci podręcznej procesora, oznacza to, że nie rozumiesz, jak to działa i nie powinieneś używać zmiennych. Programy bez blokad są bardzo trudne do poprawienia; upewnij się, że program jest poprawny, ponieważ rozumiesz, jak to działa, a nie przypadkowo).

Teraz załóżmy, że tworzysz zmienną, która jest aliasem pola nietrwałego, przekazując odniesienie do tego pola. Wewnątrz wywoływanej metody kompilator nie ma żadnego powodu, aby wiedzieć, że odwołanie musi mieć zmienną semantykę! Kompilator wesoło wygeneruje kod dla metody, która nie implementuje reguł dla zmiennych niestabilnych, ale zmienna jest zmienną . To może całkowicie zrujnować twoją logikę bez zamków; założeniem jest zawsze, że dostęp do zmiennego pola jest zawsze możliwy przy użyciu semantyki nietrwałej. Nie ma sensu traktować go czasami jako niestabilnego, a nie innym razem; musisz zawsze być konsekwentny, w przeciwnym razie nie możesz zagwarantować spójności przy innych dostępach.

Dlatego kompilator ostrzega, kiedy to robisz, ponieważ prawdopodobnie całkowicie zepsuje twoją starannie opracowaną logikę bez blokad.

Oczywiście Interlocked.Exchange jest napisane tak, aby oczekiwać zmiennego pola i postępować właściwie. Dlatego ostrzeżenie jest mylące. Bardzo tego żałuję; co powinniśmy zrobić, to zaimplementować jakiś mechanizm, za pomocą którego autor metody takiej jak Interlocked.Exchange mógłby umieścić atrybut na metodzie mówiąc „ta metoda, która pobiera ref, wymusza zmienną semantykę na zmiennej, więc pomiń ostrzeżenie”. Być może w przyszłej wersji kompilatora zrobimy to.

Eric Lippert
źródło
1
Z tego, co słyszałem, Interlocked.Exchange gwarantuje również, że powstaje bariera pamięci. Więc jeśli na przykład utworzysz nowy obiekt, przypiszesz kilka właściwości, a następnie zapiszesz obiekt w innym odwołaniu bez użycia Interlocked.Exchange, wówczas kompilator może zepsuć kolejność tych operacji, przez co dostęp do drugiego odwołania nie będzie wątkowy. bezpieczny. Czy tak jest naprawdę? Czy ma sens korzystanie z Interlocked.Exchange to tego rodzaju scenariusze?
Mike,
12
@Mike: Jeśli chodzi o to, co można zaobserwować w sytuacjach wielowątkowych z niskim zamkiem, jestem tak samo ignorantem jak następny facet. Odpowiedź prawdopodobnie będzie się różnić w zależności od procesora. Powinieneś skierować swoje pytanie do eksperta lub poczytać na ten temat, jeśli Cię interesuje. Książka Joe Duffy'ego i jego blog to dobre miejsca do rozpoczęcia. Moja zasada: nie używaj wielowątkowości. Jeśli musisz, użyj niezmiennych struktur danych. Jeśli nie możesz, użyj zamków. Tylko wtedy, gdy musisz mieć zmienne dane bez blokad, powinieneś rozważyć techniki low-lock.
Eric Lippert
Dzięki za odpowiedź Eric. Rzeczywiście mnie to interesuje, dlatego czytam książki i blogi o strategiach wielowątkowości i blokowania, a także próbuję zaimplementować je w swoim kodzie. Ale jest jeszcze wiele do nauczenia się ...
Mike,
2
@EricLippert Pomiędzy "nie używaj wielowątkowości" a "jeśli musisz, użyj niezmiennych struktur danych", wstawiłbym pośredni i bardzo powszechny poziom "mieć wątek potomny, używaj tylko obiektów wejściowych, które są wyłączną własnością, a wątek nadrzędny zużywa wyniki tylko wtedy, gdy dziecko skończy ”. Jak w var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
Jan
1
@John: To dobry pomysł. Staram się traktować wątki jak tanie procesy: są po to, aby wykonać zadanie i dać wynik, a nie być drugim wątkiem kontroli wewnątrz struktur danych programu głównego. Ale jeśli ilość pracy, jaką wykonuje wątek, jest tak duża, że ​​rozsądne jest traktowanie go jako procesu, to mówię po prostu, aby był procesem!
Eric Lippert
9

Albo twój kolega się myli, albo wie coś, czego nie ma w specyfikacji języka C #.

5.5 Atomowość zmiennych odniesień :

„Odczyty i zapisy następujących typów danych są atomowe: typy bool, char, byte, sbyte, short, ushort, uint, int, float i reference.”

Możesz więc pisać do zmiennego odniesienia bez ryzyka uzyskania uszkodzonej wartości.

Powinieneś oczywiście uważać przy podejmowaniu decyzji, który wątek powinien pobierać nowe dane, aby zminimalizować ryzyko, że zrobi to więcej niż jeden wątek naraz.

Guffa
źródło
3
@guffa: tak, też to czytałem. pozostawia to pierwotne pytanie „przypisanie odniesienia jest niepodzielne, więc dlaczego potrzebna jest funkcja Interlocked.Exchange (ref Object, Object)?” bez odpowiedzi
char m
@zebrabox: co masz na myśli? kiedy ich nie ma? co byś zrobił?
char m
@matti: Jest potrzebny, gdy musisz odczytać i zapisać wartość jako operację atomową.
Guffa
Jak często musisz się martwić, że pamięć nie zostanie poprawnie wyrównana w .NET? Ciężkie rzeczy w interopie?
Skurmedel
1
@zebrabox: Specyfikacja nie wymienia tego zastrzeżenia, daje bardzo jasne stwierdzenie. Czy masz odniesienie do sytuacji bez wyrównania do pamięci, w której odczyt lub zapis odniesienia nie jest atomowy? Wygląda na to, że naruszyłoby to bardzo jasny język w specyfikacji.
TJ Crowder
6

Interlocked.Exchange <T>

Ustawia zmienną określonego typu T na określoną wartość i zwraca oryginalną wartość jako operację niepodzielną.

Zmienia się i zwraca oryginalną wartość, jest bezużyteczna, ponieważ chcesz ją tylko zmienić i, jak powiedział Guffa, jest już atomowa.

O ile profiler nie udowodni, że jest wąskim gardłem w twojej aplikacji, powinieneś rozważyć odblokowanie blokad, łatwiej jest zrozumieć i udowodnić, że twój kod jest poprawny.

Guillaume
źródło
3

Iterlocked.Exchange() jest nie tylko atomowa, ale także dba o widoczność pamięci:

Następujące funkcje synchronizacji używają odpowiednich barier, aby zapewnić porządkowanie pamięci:

Funkcje, które wchodzą lub wychodzą z sekcji krytycznych

Funkcje sygnalizujące obiekty synchronizacji

Funkcje czekania

Funkcje powiązane

Problemy z synchronizacją i wieloma procesorami

Oznacza to, że oprócz atomowości zapewnia, że:

  • W przypadku wątku, który go wywołuje:
    • Nie dokonuje się zmiany kolejności instrukcji (przez kompilator, środowisko wykonawcze lub sprzęt).
  • Dla wszystkich wątków:
    • Żadne odczyty do pamięci, które miały miejsce przed tą instrukcją, nie zobaczą zmian wprowadzonych w tej instrukcji.
    • Wszystkie odczyty po tej instrukcji zobaczą zmianę wprowadzoną przez tę instrukcję.
    • Wszystkie zapisy do pamięci po tej instrukcji będą miały miejsce po tym, jak ta zmiana instrukcji dotrze do pamięci głównej (przez opróżnienie tej instrukcji do pamięci głównej po jej zakończeniu i nie pozwól, aby sprzęt opróżnił ją zgodnie z taktowaniem).
selalerer
źródło