Lotne vs. Zablokowane vs. Zablokowane


671

Powiedzmy, że klasa ma public int counterpole, do którego dostęp ma wiele wątków. Jest intto tylko zwiększane lub zmniejszane.

Aby zwiększyć to pole, które podejście należy zastosować i dlaczego?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • Zmień modyfikator dostępu counterna public volatile.

Teraz, gdy odkryłem volatile, usunąłem wiele lockinstrukcji i ich użycie Interlocked. Ale czy istnieje powód, aby tego nie robić?


Przeczytaj odniesienie do wątków w C # . Obejmuje tajniki twojego pytania. Każda z tych trzech ma inne cele i skutki uboczne.
spoulson

1
simple-talk.com/blogs/2012/01/24/ ... możesz zobaczyć użycie zmiennej w tablicach, nie do końca to rozumiem, ale jest to inne odniesienie do tego, co to robi.
eran otzap

50
To tak, jakby powiedzieć: „Odkryłem, że system zraszaczy nigdy nie jest aktywowany, więc zamierzam go usunąć i zastąpić czujnikami dymu”. Powodem, dla którego tego nie zrobisz, jest to, że jest niezwykle niebezpieczny i nie daje prawie żadnych korzyści . Jeśli masz czas na zmianę kodu, znajdź sposób na zmniejszenie wielowątkowości ! Nie znajdź sposobu, aby uczynić kod wielowątkowy bardziej niebezpiecznym i łatwym do złamania!
Eric Lippert,

1
Mój dom ma zarówno tryskacze, jak i czujniki dymu. Podczas zwiększania licznika na jednym wątku i odczytywania go w innym wydaje się, że potrzebujesz zarówno blokady (lub blokady), jak i niestabilnego słowa kluczowego. Prawda?
yoyo

2
@ yoyo Nie, nie potrzebujesz obu.
David Schwartz

Odpowiedzi:


859

Najgorsze (tak naprawdę nie działa)

Zmień modyfikator dostępu counternapublic volatile

Jak wspomnieli inni, samo to nie jest wcale bezpieczne. Chodzi o volatileto, że wiele wątków działających na wielu procesorach może i będzie buforować dane i ponownie zamawiać instrukcje.

Jeśli tak nie jest volatile, a CPU A zwiększa wartość, wówczas CPU B może nie zobaczyć tej zwiększonej wartości do pewnego czasu, co może powodować problemy.

Jeśli tak volatile, to tylko zapewnia, że ​​dwa procesory zobaczą te same dane w tym samym czasie. Nie powstrzymuje ich to wcale od przeplatania swoich odczytów i operacji zapisu, co jest problemem, którego próbujesz uniknąć.

Drugi najlepszy:

lock(this.locker) this.counter++;

Jest to bezpieczne (pod warunkiem, że pamiętasz lockwszędzie, gdzie masz dostęp this.counter). Zapobiega wykonywaniu przez każdy inny wątek kodu, który jest chroniony locker. Korzystanie z blokad również zapobiega problemom związanym z ponownym zamawianiem wielu procesorów, co jest świetne.

Problem polega na tym, że blokowanie jest powolne, a jeśli użyjesz ponownie lockerw innym miejscu, które nie jest tak naprawdę powiązane, możesz zablokować inne wątki bez powodu.

Najlepsza

Interlocked.Increment(ref this.counter);

Jest to bezpieczne, ponieważ skutecznie odczytuje, zwiększa i zapisuje „jedno trafienie”, którego nie można przerwać. Z tego powodu nie wpłynie to na żaden inny kod i nie musisz pamiętać o blokowaniu w innym miejscu. Jest również bardzo szybki (jak mówi MSDN, w nowoczesnych procesorach jest to często dosłownie jedna instrukcja procesora).

Nie jestem jednak do końca pewien, czy obejdzie inne procesory zmieniające porządek, czy też trzeba połączyć zmienność z przyrostem.

Zablokowane Uwagi:

  1. METODY ZABLOKOWANE SĄ WSPÓŁCZESNIE BEZPIECZNE NA KAŻDEJ liczbie rdzeni lub procesorów.
  2. Zablokowane metody stosują pełne ogrodzenie wokół wykonywanych instrukcji, więc zmiana kolejności nie następuje.
  3. Metody blokowane nie potrzebują, a nawet nie wspierają dostępu do pola niestabilnego , ponieważ zmienny jest umieszczony w połowie pola wokół operacji na danym polu, a blokowany wykorzystuje pełne ogrodzenie.

Przypis: Do tego, co lotne jest w rzeczywistości dobre.

Ponieważ volatilenie zapobiega tego rodzaju problemom z wielowątkowością, po co to jest? Dobrym przykładem jest powiedzenie, że masz dwa wątki, jeden, który zawsze zapisuje zmienną (powiedzmy queueLength), i ten, który zawsze czyta z tej samej zmiennej.

Jeśli queueLengthnie jest niestabilny, wątek A może pisać pięć razy, ale wątek B może postrzegać te zapisy jako opóźnione (lub nawet potencjalnie w niewłaściwej kolejności).

Rozwiązaniem byłoby zablokowanie, ale w tej sytuacji można również użyć niestabilności. Zapewni to, że wątek B zawsze będzie widział najbardziej aktualną rzecz, jaką napisał wątek A. Zauważ jednak, że ta logika działa tylko wtedy, gdy masz pisarzy, którzy nigdy nie czytają, i czytelników, którzy nigdy nie piszą, i jeśli to, co piszesz, jest wartością atomową. Jak tylko wykonasz pojedynczy odczyt-modyfikacja-zapis, musisz przejść do operacji zablokowanych lub użyć blokady.


29
„Nie jestem do końca pewien… czy trzeba także łączyć zmienność z przyrostem”. Nie można ich łączyć AFAIK, ponieważ nie możemy przekazać lotnego przez ref. Nawiasem mówiąc, świetna odpowiedź.
Hosam Aly

41
Wielkie dzięki! Twój przypis „Do czego właściwie służy lotność” jest tym, czego szukałem i potwierdziłem, jak chcę używać lotności.
Jacques Bosch

7
Innymi słowy, jeśli var zostanie zadeklarowane jako zmienne, kompilator przyjmie, że wartość var ​​nie pozostanie taka sama (tj. Zmienna) za każdym razem, gdy napotkasz kod. Tak więc w pętli, takiej jak: podczas gdy (m_Var) {}, a m_Var jest ustawiony na false w innym wątku, kompilator nie będzie po prostu sprawdzał, co jest już w rejestrze, który został wcześniej załadowany wartością m_Var, ale odczytuje wartość z m_Var jeszcze raz. Nie oznacza to jednak, że brak deklaracji niestabilności spowoduje, że pętla będzie działała w nieskończoność - określenie zmiennej niestabilnej gwarantuje tylko, że nie będzie, jeśli m_Var ma wartość false w innym wątku.
Zach Saw

35
@Zach Saw: W modelu pamięci dla C ++ zmienność jest taka, jak ją opisałeś (zasadniczo przydatny w przypadku pamięci mapowanej na urządzenie i niewiele więcej). Zgodnie z modelem pamięci dla CLR (to pytanie jest oznaczone jako C #), lotne wstawi bariery pamięci wokół odczytów i zapisów w tym miejscu przechowywania. Bariery pamięci (i specjalne zablokowane warianty niektórych instrukcji montażu) czy mówisz procesorowi, aby nie zmieniał kolejności rzeczy, a są one dość ważne ...
Orion Edwards

19
@ZachSaw: Zmienne pole w języku C # uniemożliwia kompilatorowi C # i kompilatorowi jit wykonanie pewnych optymalizacji, które buforowałyby wartość. Zapewnia również pewne gwarancje dotyczące tego, w jakiej kolejności można odczytywać i zapisywać wiele wątków. Jako szczegół implementacji może to zrobić, wprowadzając bariery pamięci dla odczytów i zapisów. Dokładna semantyka gwarantowana jest opisana w specyfikacji; należy pamiętać, że specyfikacja nie gwarantuje, że wszystkie wątki będą przestrzegały spójnej kolejności wszystkich lotnych zapisów i odczytów .
Eric Lippert,

147

EDYCJA: Jak zauważono w komentarzach, obecnie z przyjemnością używam Interlockedprzypadków pojedynczej zmiennej, gdzie jest to oczywiście w porządku. Kiedy stanie się bardziej skomplikowane, nadal wrócę do blokowania ...

Używanie volatilenie pomoże, gdy trzeba zwiększyć - ponieważ odczyt i zapis to osobne instrukcje. Kolejny wątek może zmienić wartość po przeczytaniu, ale przed odpisem.

Osobiście prawie zawsze po prostu blokuję - łatwiej jest uzyskać właściwy sposób, który jest oczywiście właściwy niż zmienność lub Zablokowany. Jeśli o mnie chodzi, wielowątkowość bez blokady jest przeznaczona dla prawdziwych ekspertów od gwintowania, z których nie jestem jednym. Jeśli Joe Duffy i jego zespół zbudują ładne biblioteki, które zrównoleglą rzeczy bez blokowania tak dużo, jak coś, co zbuduję, to wspaniałe, i użyję tego w mgnieniu oka - ale kiedy robię wątki sam, próbuję nie komplikuj.


16
+1 za zapomnienie o kodowaniu bez blokady.
Xaqron

5
kody bez blokady zdecydowanie nie są tak naprawdę bez blokady, ponieważ blokują się na pewnym etapie - niezależnie od tego, czy na poziomie magistrali (FSB), czy na poziomie interCPU, wciąż trzeba zapłacić karę. Jednak blokowanie na niższych poziomach jest generalnie szybsze, o ile nie nasycasz przepustowości miejsca, w którym następuje blokada.
Zach Saw

2
Nie ma nic złego w Interlocked, jest to dokładnie to, czego szukasz i szybsze niż pełna blokada ()
Jaap

5
@Jaap: Tak, w dzisiejszych czasach używałbym blokady dla prawdziwego pojedynczego licznika. Po prostu nie chciałbym zaczynać bawić się, próbując wypracować interakcje między wieloma bezobsługowymi aktualizacjami zmiennych.
Jon Skeet

6
@ZachSaw: Twój drugi komentarz mówi, że zablokowane operacje „blokują” się na pewnym etapie; termin „blokada” ogólnie oznacza, że ​​jedno zadanie może zachować wyłączną kontrolę nad zasobem przez nieograniczony czas; podstawową zaletą programowania bez blokady jest to, że unika się niebezpieczeństwa, że ​​zasoby staną się bezużyteczne w wyniku uporządkowania zadania właściciela. Synchronizacja magistrali używana przez blokowaną klasę jest nie tylko „ogólnie szybsza” - w większości systemów ma ograniczony czas najgorszego przypadku, podczas gdy blokady nie.
supercat

44

volatile” nie zastępuje Interlocked.Increment! Po prostu upewnia się, że zmienna nie jest buforowana, ale używana bezpośrednio.

Zwiększenie zmiennej wymaga w rzeczywistości trzech operacji:

  1. czytać
  2. przyrost
  3. pisać

Interlocked.Increment wykonuje wszystkie trzy części jako pojedynczą operację atomową.


4
Innymi słowy, zablokowane zmiany są w pełni ogrodzone i jako takie są atomowe. Lotne elementy są tylko częściowo ogrodzone i jako takie nie można zagwarantować, że są bezpieczne.
JoeGeeky,

1
Faktycznie, volatilenie nie upewnij się, że zmienna nie jest buforowane. Po prostu nakłada ograniczenia na sposób buforowania. Na przykład nadal można go buforować w pamięci podręcznej L2 procesora, ponieważ są one spójne sprzętowo. Nadal można go wybrać. Zapisy mogą być nadal wysyłane do pamięci podręcznej i tak dalej. (Myślę, że o to właśnie chodziło Zachowi.)
David Schwartz,

42

Zależnie od tego, czy szukasz blokady, czy blokady.

Zmienna z pewnością nie jest tym, czego szukasz - po prostu mówi kompilatorowi, aby traktował zmienną jako zawsze zmieniającą się, nawet jeśli bieżąca ścieżka kodu pozwala kompilatorowi zoptymalizować odczyt z pamięci.

na przykład

while (m_Var)
{ }

jeśli m_Var jest ustawiony na false w innym wątku, ale nie jest zadeklarowany jako niestabilny, kompilator może zrobić z niego nieskończoną pętlę (ale nie oznacza to, że zawsze będzie), sprawdzając go względem rejestru procesora (np. EAX, ponieważ to był do czego m_Var był pobierany od samego początku) zamiast wydawania kolejnego odczytu do lokalizacji pamięci m_Var (może to być buforowane - nie wiemy i nie przejmujemy się i to jest punkt spójności pamięci podręcznej x86 / x64). Wszystkie posty wcześniejszych osób, które wspominały o zmianie kolejności instrukcji, po prostu pokazują, że nie rozumieją architektury x86 / x64. Lotne niewydaje bariery odczytu / zapisu, jak sugerują wcześniejsze posty, mówiąc: „zapobiega zmianie kolejności”. W rzeczywistości, dzięki protokołowi MESI, mamy gwarancję, że odczytany wynik jest zawsze taki sam we wszystkich procesorach, niezależnie od tego, czy rzeczywiste wyniki zostały wycofane do pamięci fizycznej, czy po prostu znajdują się w pamięci podręcznej lokalnego procesora. Nie będę zagłębiał się w szczegóły tego, ale zapewniam cię, że jeśli coś pójdzie nie tak, Intel / AMD prawdopodobnie spowoduje wycofanie procesora! Oznacza to również, że nie musimy przejmować się realizacją zleceń itp. Zawsze gwarantujemy, że wyniki przejdą na emeryturę - w przeciwnym razie jesteśmy wypchani!

Dzięki Interlocked Increment procesor musi wyjść, pobrać wartość z podanego adresu, a następnie zwiększyć i zapisać go z powrotem - wszystko to, mając wyłączną własność całej linii pamięci podręcznej (blokada xadd), aby upewnić się, że żadne inne procesory nie mogą modyfikować jego wartość.

W przypadku lotności nadal będziesz mieć tylko 1 instrukcję (zakładając, że JIT jest tak wydajny, jak powinien) - inc dword ptr [m_Var]. Jednak procesor (cpuA) nie prosi o wyłączną własność linii pamięci podręcznej, robiąc wszystko, co zrobił z wersją zablokowaną. Jak możesz sobie wyobrazić, oznacza to, że inne procesory mogą zapisać zaktualizowaną wartość z powrotem do m_Var po jej odczytaniu przez cpuA. Więc zamiast teraz dwukrotnie zwiększać wartość, kończy się to tylko raz.

Mam nadzieję, że to rozwiąże problem.

Aby uzyskać więcej informacji, zobacz „Zrozumienie wpływu technik Low-Lock w aplikacjach wielowątkowych” - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps Co skłoniło tę bardzo późną odpowiedź? Wszystkie odpowiedzi były tak rażąco niepoprawne (szczególnie ta oznaczona jako odpowiedź) w ich wyjaśnieniach, że musiałem to wyjaśnić dla każdego, kto to czyta. wzrusza ramionami

pps Zakładam, że celem jest x86 / x64, a nie IA64 (ma inny model pamięci). Zauważ, że specyfikacje ECMA Microsoftu są zepsute, ponieważ określają najsłabszy model pamięci zamiast najsilniejszego (zawsze lepiej jest określić w oparciu o najsilniejszy model pamięci, aby był spójny na różnych platformach - w przeciwnym razie kod, który działałby 24-7 na x86 / x64 może w ogóle nie działać na IA64, chociaż Intel zaimplementował podobnie silny model pamięci dla IA64) - Microsoft sam to przyznał - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .


3
Ciekawy. Czy możesz to odnieść? Z radością zagłosuję za tym, ale publikowanie w jakimś agresywnym języku 3 lata po wysoko głosowanej odpowiedzi zgodnej z zasobami, które przeczytałem, będzie wymagało bardziej konkretnego dowodu.
Steven Evers,

Jeśli potrafisz wskazać, do której części chcesz się odwoływać, chętnie skopałbym skądś pewne rzeczy (bardzo wątpię, że ujawniłem jakieś tajemnice handlowe dostawców x86 / x64, więc powinny być łatwo dostępne poza wiki, Intel PRM (podręczniki programisty), blogi MSFT, MSDN lub coś podobnego) ...
Zach Saw

2
Dlaczego ktoś chciałby zapobiec buforowaniu procesora, jest poza mną. Cała nieruchomość (zdecydowanie nieistotna pod względem wielkości i kosztu) przeznaczona na spójność pamięci podręcznej jest całkowicie marnowana, jeśli tak jest ... Chyba że nie potrzebujesz żadnej pamięci podręcznej, takiej jak karta graficzna, urządzenie PCI itp., Nie ustawiłbyś linia bufora do zapisu.
Zach Saw

4
Tak, wszystko, co mówisz, to jeśli nie 100%, przynajmniej 99% na znaku. Ta strona jest (głównie) bardzo przydatna, gdy spieszysz się z rozwojem w pracy, ale niestety nie ma tam dokładności odpowiedzi odpowiadających (grze) głosami. Tak więc w zasadzie w przepełnieniu stosu możesz poczuć, jakie jest popularne rozumienie czytelników, a nie to, co tak naprawdę jest. Czasami najlepsze odpowiedzi to po prostu bełkot - miłe mity. I niestety właśnie to rodzi ludzi, którzy spotykają się z czytaniem podczas rozwiązywania problemu. Jest to zrozumiałe, nikt nie może wiedzieć wszystkiego.
user1416420,

1
@BenVoigt Mógłbym kontynuować i odpowiedzieć na temat wszystkich architektur .NET działa, ale zajmie to kilka stron i zdecydowanie nie jest odpowiedni dla SO. Zdecydowanie lepiej jest edukować ludzi w oparciu o najczęściej używany model memu sprzętowego .NET niż ten, który jest arbitralny. A moimi komentarzami „wszędzie” poprawiałem błędy, które popełniali ludzie, zakładając opróżnianie / unieważnianie pamięci podręcznej itp. Przyjęli założenia dotyczące podstawowego sprzętu bez określania, który to sprzęt.
Zach Saw

16

Zablokowane funkcje nie blokują się. Są atomowe, co oznacza, że ​​mogą wykonać bez możliwości zmiany kontekstu podczas przyrostu. Nie ma więc szans na impas ani czekanie.

Powiedziałbym, że zawsze powinieneś preferować blokadę i przyrost.

Zmienna jest przydatna, jeśli chcesz, aby zapisy w jednym wątku były czytane w innym, i jeśli chcesz, aby optymalizator nie zmieniał kolejności operacji na zmiennej (ponieważ dzieje się w innym wątku, o którym optymalizator nie wie). Jest to ortogonalny wybór sposobu zwiększania.

To jest naprawdę dobry artykuł, jeśli chcesz przeczytać więcej o kodzie bez zamka i odpowiedni sposób na napisanie go

http://www.ddj.com/hpc-high-performance-computing/210604448


11

lock (...) działa, ale może zablokować wątek i może spowodować zakleszczenie, jeśli inny kod używa tych samych blokad w sposób niezgodny.

Interlocked. * To właściwy sposób, aby to zrobić ... znacznie mniej narzutów, ponieważ nowoczesne procesory obsługują to jako prymitywne.

sam lotny nie jest poprawny. Wątek, który próbuje pobrać, a następnie zapisać zmodyfikowaną wartość, może nadal powodować konflikt z innym wątkiem, który robi to samo.


8

Zrobiłem test, aby zobaczyć, jak działa teoria: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Mój test był bardziej skoncentrowany na CompareExchnage, ale wynik dla Increment jest podobny. Zablokowane nie jest konieczne szybciej w środowisku z wieloma procesorami. Oto wynik testu przyrostu na 2-letnim serwerze z 16 procesorami. Należy pamiętać, że test obejmuje również bezpieczny odczyt po wzroście, co jest typowe w prawdziwym świecie.

D:\>InterlockVsMonitor.exe 16
Using 16 threads:
          InterlockAtomic.RunIncrement         (ns):   8355 Average,   8302 Minimal,   8409 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):   7077 Average,   6843 Minimal,   7243 Maxmial

D:\>InterlockVsMonitor.exe 4
Using 4 threads:
          InterlockAtomic.RunIncrement         (ns):   4319 Average,   4319 Minimal,   4321 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):    933 Average,    802 Minimal,   1018 Maxmial

Próbka kodu, którą przetestowałeś, była jednak tak trywialna - naprawdę nie ma sensu testowanie jej w ten sposób! Najlepiej byłoby zrozumieć, co faktycznie robią różne metody, i zastosować odpowiednią w zależności od scenariusza użytkowania.
Zach Saw

@Zach, jak tu była dyskusja na temat scenariusza zwiększenia licznika w sposób bezpieczny dla wątków. Jaki inny scenariusz użycia miałeś na myśli lub jak byś go przetestował? Dzięki za komentarz BTW.
Kenneth Xu

Chodzi o to, że to sztuczny test. Nie będziesz wbijać tej samej lokalizacji, która często występuje w prawdziwym świecie. Jeśli tak, to dobrze, że masz wąskie gardło FSB (jak pokazano w skrzynkach serwera). W każdym razie spójrz na moją odpowiedź na swoim blogu.
Zach Saw

2
Patrząc wstecz. Jeśli prawdziwe wąskie gardło występuje w FSB, implementacja monitora powinna przestrzegać tego samego wąskiego gardła. Prawdziwa różnica polega na tym, że Interlocked wykonuje zajęte czekanie i ponawianie, co staje się prawdziwym problemem przy liczeniu wysokiej wydajności. Przynajmniej mam nadzieję, że mój komentarz zwróci uwagę, że Interlocked nie zawsze jest właściwym wyborem do liczenia. Fakt, że ludzie szukają alternatyw, dobrze to wyjaśnił. Potrzebujesz długiego sumatora gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Kenneth Xu

8

3

Chciałbym dodać do wspomniano w innych odpowiedzi różnica między volatile, Interlockedoraz lock:

Zmienne słowo kluczowe można zastosować do pól tego typu :

  • Typy referencyjne
  • Typy wskaźników (w niebezpiecznym kontekście). Zauważ, że chociaż sam wskaźnik może być lotny, obiekt, na który wskazuje, nie może. Innymi słowy, nie można zadeklarować „wskaźnika” jako „niestabilnego”.
  • Proste typy takie jak sbyte, byte, short, ushort, int, uint, char, float, i bool.
  • Typ wyliczenia z jednym z następujących podstawowych typów: byte, sbyte, short, ushort, intlub uint.
  • Ogólne parametry typu, o których wiadomo, że są typami odniesienia.
  • IntPtra UIntPtr.

Inne typy , w tym doublei long, nie mogą być oznaczone jako „lotne”, ponieważ nie można zagwarantować, że odczytuje i zapisuje w polach tego typu jako atomowe. Aby chronić wielowątkowy dostęp do tego typu pól, użyj Interlockedczłonków klasy lub chroń dostęp za pomocą lockinstrukcji.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.