Powiedzmy, że klasa ma public int counter
pole, do którego dostęp ma wiele wątków. Jest int
to 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
counter
napublic volatile
.
Teraz, gdy odkryłem volatile
, usunąłem wiele lock
instrukcji i ich użycie Interlocked
. Ale czy istnieje powód, aby tego nie robić?
Odpowiedzi:
Najgorsze (tak naprawdę nie działa)
Jak wspomnieli inni, samo to nie jest wcale bezpieczne. Chodzi o
volatile
to, ż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:
Jest to bezpieczne (pod warunkiem, że pamiętasz
lock
wszędzie, gdzie masz dostępthis.counter
). Zapobiega wykonywaniu przez każdy inny wątek kodu, który jest chronionylocker
. 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
locker
w innym miejscu, które nie jest tak naprawdę powiązane, możesz zablokować inne wątki bez powodu.Najlepsza
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:
Przypis: Do tego, co lotne jest w rzeczywistości dobre.
Ponieważ
volatile
nie 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ą (powiedzmyqueueLength
), i ten, który zawsze czyta z tej samej zmiennej.Jeśli
queueLength
nie 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.
źródło
EDYCJA: Jak zauważono w komentarzach, obecnie z przyjemnością używam
Interlocked
przypadków pojedynczej zmiennej, gdzie jest to oczywiście w porządku. Kiedy stanie się bardziej skomplikowane, nadal wrócę do blokowania ...Używanie
volatile
nie 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.
źródło
„
volatile
” nie zastępujeInterlocked.Increment
! Po prostu upewnia się, że zmienna nie jest buforowana, ale używana bezpośrednio.Zwiększenie zmiennej wymaga w rzeczywistości trzech operacji:
Interlocked.Increment
wykonuje wszystkie trzy części jako pojedynczą operację atomową.źródło
volatile
nie 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.)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
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 .
źródło
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
źródło
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.
źródło
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.
źródło
Wspieram odpowiedź Jona Skeeta i chcę dodać następujące linki dla wszystkich, którzy chcą dowiedzieć się więcej o „niestabilnych” i zablokowanych:
Atomowość, lotność i niezmienność są różne, część pierwsza - (Fabulous Adventures In Coding Erica Lipperta)
Atomowość, lotność i niezmienność są różne, część druga
Atomowość, lotność i niezmienność są różne, część trzecia
Sayonara Volatile - (migawka Wayback Machine z blogu Joe Duffy, jak pojawił się w 2012 roku)
źródło
Chciałbym dodać do wspomniano w innych odpowiedzi różnica między
volatile
,Interlocked
orazlock
:Zmienne słowo kluczowe można zastosować do pól tego typu :
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
, ibool
.byte
,sbyte
,short
, ushort,int
lubuint
.IntPtr
aUIntPtr
.Inne typy , w tym
double
ilong
, 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żyjInterlocked
członków klasy lub chroń dostęp za pomocąlock
instrukcji.źródło