Multi-threading bez blokady jest dla prawdziwych ekspertów od gwintowania

Czytałem ODPOWIEDŹ , którąJon Skeet dał na pytanie i w nim wspomniał o tym:

Jeśli chodzi o mnie, multi-threading bez blokady jest dla prawdziwych ekspertów od gwintowania, z których nie jestem jednym.

To nie pierwszy raz, kiedy to słyszę, ale bardzo niewielu ludzi mówi o tym, jak naprawdę to robisz, jeśli jesteś zainteresowany nauką pisania kodu wielowątkowego bez blokady.

Więc moje pytanie jest poza uczenie się wszystkiego na temat wątków itp. gdzie zacząć próbować nauczyć się pisać kod wielowątkowy bez blokady i jakie są dobre zasoby.

Cheers

Author: Community, 2010-03-27

6 answers

Obecne implementacje typu "lock-free" przez większość czasu podążają za tym samym wzorcem:

  • *przeczytaj jakiś stan i zrób jego kopię * *
  • *modify copy * *
  • wykonaj operację z blokadą
  • spróbuj ponownie, jeśli się nie powiedzie

(*opcjonalnie: zależy od struktury danych / algorytmu)

Ostatni bit jest niesamowicie podobny do spinlocka. W rzeczywistości jest to podstawowy spinlock. :)
Zgadzam się z @nobugz w tej kwestii: koszt połączenia operacje używane w wielowątkowości bez blokady są zdominowane przez zadania cache i memory-coherency, które musi wykonywać.

to, co zyskujesz dzięki strukturze danych, która jest "wolna od blokad", to to, że Twoje" blokady " są bardzo drobnoziarniste. Zmniejsza to prawdopodobieństwo, że dwa równoległe wątki uzyskają dostęp do tej samej "blokady" (lokalizacji pamięci).

Sztuczka najczęściej polega na tym, że nie masz dedykowanych blokad - zamiast tego traktujesz np. wszystkie elementy w tablicy lub wszystkie węzły w powiązanej liście jako "spin-lock". Czytasz, modyfikujesz i próbujesz zaktualizować, jeśli nie było aktualizacji od ostatniego czytania. Jeśli tak, spróbuj ponownie.
To sprawia, że "blokowanie" (oh, sorry, non-locking :) jest bardzo drobnoziarniste, bez wprowadzania dodatkowych wymagań dotyczących pamięci lub zasobów.
Czyniąc go bardziej drobnoziarnistym zmniejsza prawdopodobieństwo oczekiwania. Wykonanie go tak drobnoziarnistego, jak to tylko możliwe, bez wprowadzania dodatkowych wymagań dotyczących zasobów brzmi świetnie, prawda?

Większość zabawy jednak może pochodzić z zapewniając prawidłowe ładowanie / zamawianie sklepu .
Wbrew własnej intuicji, procesory mogą dowolnie zmieniać kolejność odczytów/zapisów pamięci - przy okazji są bardzo inteligentne: będziesz miał problem z obserwacją tego z jednego wątku. Pojawią się jednak problemy, gdy zaczniesz robić wielowątkowość na wielu rdzeniach. Twoja intuicja ulegnie załamaniu: to, że instrukcja jest wcześniej w Twoim kodzie, nie oznacza, że faktycznie stanie się wcześniej. Procesory mogą przetwarzać instrukcje nie w porządku: a szczególnie lubią to robić instrukcje z dostępem do pamięci, aby ukryć opóźnienie pamięci głównej i lepiej wykorzystać ich pamięć podręczną.

Teraz, jest pewne wbrew intuicji, że sekwencja kodu nie płynie "z góry na dół", zamiast tego biegnie tak, jakby w ogóle nie było sekwencji - i może być nazywana "devil' s playground". Uważam, że nie jest możliwe podanie dokładnej odpowiedzi na to, jakie zmiany w ładowaniu/przechowywaniu będą miały miejsce. Zamiast tego zawsze mówi się w kategoriach mays i mights i puszki i przygotować się na najgorsze. "Och, procesor Może zmienić kolejność odczytu, aby przyszedł przed tym zapisem, więc najlepiej jest umieścić barierę pamięci tutaj, w tym miejscu."

Sprawy komplikuje fakt, że nawet te mays i mights mogą się różnić w zależności od architektury procesora. Może to być na przykład przypadek, że coś, co jest {15]}gwarantowane, że nie wydarzy się {16]} w jednej architekturze może się zdarzyć na innym.


Aby uzyskać" bez blokady " wielowątkowość, musisz zrozumieć modele pamięci.
Uzyskanie poprawnego modelu pamięci i gwarancji nie jest jednak trywialne, o czym świadczy ta historia, w której Intel i AMD wprowadzili pewne poprawki do dokumentacji MFENCE, powodując pewne zamieszanie wśród programistów JVM. Jak się okazało, dokumentacja, na której od początku opierali się programiści, nie była tak precyzyjna w pierwszym miejsce.

Blokady w. NET powodują ukrytą barierę pamięci, dzięki czemu możesz bezpiecznie z nich korzystać (przez większość czasu, czyli... zobacz na przykład to Joe Duffy-Brad Abrams-Vance Morrison wielkość na leniwe inicjalizacji, zamki, lotnych i bariery pamięci. :) (Pamiętaj, aby podążać za linkami na tej stronie.)

[1]} jako dodatkowy bonus, poznasz model pamięci. NET podczas zadania pobocznego [21]}. :) Jest też "oldie but goldie" z Vance ' a Morrison: Co Każdy Dev Musi Wiedzieć O Wielowątkowych Aplikacjach .

...i oczywiście, jak @Eric wspomniał, Joe Duffy jest definitywną lekturą na ten temat.

Dobry STM może zbliżyć się do drobnoziarnistego blokowania i prawdopodobnie zapewni wydajność zbliżoną lub dorównującą ręcznie wykonanej implementacji. Jednym z nich jest STM.NET z DevLabs projects Z MS.

Jeśli nie jesteś fanatykiem. NET, Doug Lea wykonał świetną robotę w JSR-166 .
Cliff Click ma ciekawe podejście do tabel hashowych, które nie polegają na blokowaniu pasków-jak to robią równoległe tabele hashowe Java i. NET - i wydają się dobrze skalować do 750 procesorów.

Jeśli nie boisz się wkroczyć na terytorium Linuksa, poniższy artykuł zawiera więcej informacji na temat wewnętrznych aspektów obecnych architektur pamięci i tego, jak współdzielenie pamięci podręcznej może zniszczyć wydajność: co każdy programista powinien wiedzieć o pamięci .

@Ben poczynił wiele komentarzy na temat MPI: szczerze zgadzam się, że MPI może zabłysnąć w niektórych obszarach. Rozwiązanie oparte na MPI może być łatwiejsze do zrozumienia, łatwiejsze do wdrożenia i mniej podatne na błędy niż na wpół upieczona implementacja blokowania, która stara się być inteligentna. (Dotyczy to jednak-subiektywnie-również rozwiązania opartego na STM.) Założę się też, że latami świetlnymi łatwiej jest poprawnie napisać porządną rozproszoną aplikację w np. Erlang, jak wiele udanych przykłady sugerują.

MPI ma jednak własne koszty i własne problemy, gdy jest uruchamiany na pojedynczym, wielordzeniowym systemie. Na przykład w Erlang, istnieją problemy do rozwiązania wokół synchronizacji harmonogramu procesów i kolejek wiadomości.
Ponadto, systemy MPI zazwyczaj implementują rodzaj współpracy N:m scheduling dla "lekkich procesów". Oznacza to na przykład, że istnieje nieunikniona zmiana kontekstu między lekkimi procesy. To prawda, że nie jest to "klasyczny przełącznik kontekstowy", ale przede wszystkim operacja przestrzeni użytkownika i może być wykonana szybko - jednak szczerze wątpię, że może być przeprowadzona w ramach 20-200 cykli operacja zblokowana trwa. Przełączanie kontekstu w trybie użytkownika jest z pewnością wolniejsze nawet w Bibliotece Intel McRT. Planowanie N: M z lekkimi procesami nie jest nowością. LWP były tam przez długi czas w Solarisie. Zostały porzucone. W NT były włókna. Są to obecnie głównie relikt. W NetBSD były "aktywacje". Zostały porzucone. Linux miał swoje własne podejście do tematu N: m threading. Wygląda na to, że już nie żyje.
Od czasu do czasu pojawiają się nowe pretendenty: na przykład McRT od Intela, lub ostatnio user-Mode Scheduling wraz z ConCRT od Microsoftu.
Na najniższym poziomie robią to, co robi n: M MPI scheduler. Erlang - lub dowolny system MPI -, może znacznie skorzystać na systemach SMP wykorzystując nowy UMS .

Wydaje mi się, że pytanie OP Nie dotyczy zasadności i subiektywnych argumentów za/przeciw jakiemukolwiek rozwiązaniu, ale jeśli miałbym odpowiedzieć na to pytanie, to chyba zależy to od zadania: do budowy niskopoziomowych, wysokowydajnych podstawowych struktur danych, które działają na pojedynczym systemie z wieloma rdzeniami , albo techniki low-lock / "lock-free", albo STM dadzą najlepsze wyniki pod względem wydajności i prawdopodobnie pokonają rozwiązanie MPI w dowolnym momencie. wydajność, nawet jeśli powyższe zmarszczki są wyprostowane np. w Erlang.
Do budowy czegoś umiarkowanie bardziej skomplikowanego, co działa na jednym systemie, wybrałbym być może Klasyczne gruboziarniste blokowanie lub, jeśli chodzi o wydajność, STM.
Aby zbudować system rozproszony, system MPI prawdopodobnie dokonałby naturalnego wyboru.
Zauważ, że istnieją implementacje MPI dla . NET oraz (choć wydają się nie być tak aktywne).

 93
Author: Andras Vass,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:26

Książka Joe Duffy:

Http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

Pisze również bloga na te tematy.

Sztuką poprawnego działania programów low-lock jest zrozumienie na głębokim poziomie dokładnie jakie są zasady modelu pamięci dla konkretnej kombinacji sprzętu, systemu operacyjnego i środowiska uruchomieniowego.

Ja osobiście nie jestem na tyle inteligentny, by poprawnie programować low-lock poza InterlockedIncrement, ale jeśli jesteś, świetnie, idź na to. Po prostu upewnij się, że zostawiasz w kodzie dużo dokumentacji, aby osoby, które nie są tak inteligentne jak ty, nie przypadkowo złamały jednego z niezmienników modelu pamięci i nie wprowadziły niemożliwego do znalezienia błędu.

 27
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-27 15:12:44

W dzisiejszych czasach nie ma czegoś takiego jak "gwintowanie bez blokady". Był to interesujący plac zabaw dla środowisk akademickich i tym podobnych, jeszcze pod koniec ubiegłego wieku, kiedy sprzęt komputerowy był powolny i drogi. algorytm Dekkera był zawsze moim ulubionym, nowoczesny sprzęt wypuścił go na pastwisko. To już nie działa.

Zakończyły się dwa wydarzenia: rosnąca dysproporcja między szybkością pamięci RAM a procesorem. I zdolność producentów chipów do umieszczenia więcej niż jednego Rdzeń PROCESORA na chipie.

Problem z szybkością pamięci RAM wymagał od projektantów chipów umieszczenia bufora na chipie procesora. Bufor przechowuje kod i dane, szybko dostępne przez rdzeń PROCESORA. I mogą być odczytywane i zapisywane z / DO PAMIĘCI RAM w znacznie wolniejszym tempie. Bufor ten nazywany jest cache CPU, większość procesorów ma co najmniej dwa z nich. Pamięć podręczna pierwszego poziomu jest mała i szybka, druga jest duża i wolniejsza. Tak długo, jak procesor może odczytywać dane i instrukcje z pamięci podręcznej 1. poziomu, będzie działać szybko. A Cache miss jest naprawdę drogi, układa procesor w stan uśpienia na 10 cykli, jeśli dane nie są w 1. cache, aż 200 cykli, jeśli nie są w 2. cache i trzeba je odczytać z pamięci RAM.

Każdy rdzeń CPU ma własną pamięć podręczną, przechowuje własny" widok " pamięci RAM. Gdy procesor zapisuje dane, zapis jest dokonywany do pamięci podręcznej, która jest następnie powoli spłukiwana DO PAMIĘCI RAM. Nieuniknione, każdy rdzeń będzie teraz miał inny widok zawartości pamięci RAM. Innymi słowy, jeden procesor nie wie, jaki inny procesor zapisuje do czasu zakończenia cyklu zapisu pamięci Ram i PROCESOR odświeża swój własny widok.

To jest dramatycznie niezgodne z threading. Zawsze naprawdę obchodzi cię, jaki jest stan innego wątku, kiedy musisz odczytać dane, które zostały napisane przez inny wątek. Aby to zapewnić, musisz jawnie zaprogramować tak zwaną barierę pamięci. Jest to prymitywny procesor niskopoziomowy, który zapewnia, że wszystkie pamięci podręczne CPU są w spójnym stanie i mają aktualny widok pamięci RAM. Wszystkie oczekujące zapisy muszą zostać przepłukane DO PAMIĘCI RAM, pamięci podręczne muszą zostać odświeżone.

Jest to dostępne w.NET, wątku.Metoda MemoryBarrier () implementuje 1. Biorąc pod uwagę, że jest to 90% zadania, które wykonuje Instrukcja lock (i 95+% czasu wykonania), po prostu nie jesteś do przodu, unikając narzędzi, które oferuje. NET i próbując zaimplementować własne.

 19
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-27 15:01:14

Google dla blokuje wolne struktury danych i Pamięci transakcyjnej oprogramowania.

Zgodzę się z Johnem Skeetem w tej sprawie; gwintowanie bez blokady jest diabelskim placem zabaw i najlepiej pozostawić ludziom, którzy wiedzą, że wiedzą to, co powinni wiedzieć.

 6
Author: Marcelo Cantos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-27 13:35:08

Jeśli chodzi o wielowątkowość, musisz dokładnie wiedzieć, co robisz. Mam na myśli zbadanie wszystkich możliwych scenariuszy/przypadków, które mogą wystąpić podczas pracy w środowisku wielowątkowym. Wielowątkowość bez blokady nie jest biblioteką ani klasą, którą włączamy, jest wiedzą/doświadczeniem, które zdobywamy podczas naszej podróży na wątkach.

 0
Author: bragboy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-27 10:54:10

Chociaż wątki bez blokady mogą być trudne w. NET, często można wprowadzić znaczące ulepszenia podczas używania blokady, badając dokładnie, co należy zablokować, i minimalizując zablokowaną sekcję... jest to również znane jako minimalizacja ziarnistości blokady .

Jako przykład po prostu powiedz, że musisz zabezpieczyć wątek kolekcji. Nie rzucaj na oślep blokady wokół metody iteracyjnej nad kolekcją, jeśli wykonuje ona jakieś zadanie obciążające procesor na każdym elemencie. Ty może Wystarczy umieścić blokadę wokół tworząc płytką kopię kolekcji. Iteracja nad kopią może wtedy działać bez blokady. Oczywiście jest to bardzo zależne od specyfiki Twojego kodu, ale udało mi się naprawić Lock convoy Problem z tym podejściem.

 0
Author: dodgy_coder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-20 04:14:17