Wielowątkowość bez blokady jest przeznaczona dla prawdziwych ekspertów od wątków


86

Czytałem odpowiedź, którą Jon Skeet udzielił na pytanie i wspomniał w niej:

Jeśli o mnie chodzi, wielowątkowość bez blokad jest dla prawdziwych ekspertów od wątków, z których nie jestem.

To nie pierwszy raz, kiedy to słyszę, ale bardzo niewiele osób mówi o tym, jak to się robi, jeśli jesteś zainteresowany nauczeniem się pisania wielowątkowego kodu bez blokad.

Więc moje pytanie brzmi, poza tym, że nauczysz się wszystkiego, co możesz o wątkach itp., Gdzie zaczniesz próbować nauczyć się pisać kod wielowątkowy bez blokad i jakie są dobre zasoby.

Twoje zdrowie


Używam platform gcc, linux i X86 / X68. Bez blokad nie jest tak trudne, jak wszystkie sprawiają, że brzmi! Wbudowane gcc atomowe mają bariery pamięciowe na danych Intel, ale to nie ma znaczenia w prawdziwym życiu. Liczy się to, że pamięć jest modyfikowana atomowo. Podczas projektowania struktur danych „bez blokad” po prostu trzęsie się, że nie ma znaczenia, kiedy inny wątek zauważy zmianę. Pojedyncze połączone listy, listy pomijane, tabele skrótów, listy bezpłatne itp. Są dość łatwe do wykonania bez blokowania. Blokada nie jest na wszystko. To tylko kolejne narzędzie, które jest odpowiednie w określonych sytuacjach.
johnnycrash


Głosowanie za zamknięciem jako rekomendacja zasobu lub brak jasności, o co prosisz.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:


100

Obecne implementacje „bez blokad” przez większość czasu działają według tego samego wzorca:

  • * przeczytaj stan i zrób jego kopię **
  • * modyfikuj kopię **
  • wykonać operację blokowaną
  • spróbuj ponownie, jeśli się nie powiedzie

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

Ostatni kawałek jest niesamowicie podobny do spinlocka. W rzeczywistości jest to podstawowy spinlock . :)
Zgadzam się z @nobugz co do tego: koszt operacji blokowanych używanych w wielowątkowości bez blokad jest zdominowany przez zadania związane z pamięcią podręczną i spójnością pamięci, które musi wykonać .

Jednak dzięki strukturze danych „wolnej od blokad” zyskujesz to, że Twoje „blokady” są bardzo drobnoziarniste . Zmniejsza to prawdopodobieństwo, że dwa współbieżne wątki będą miały dostęp do tej samej „blokady” (lokalizacji pamięci).

W większości przypadków sztuczka polega na tym, że nie masz dedykowanych blokad - zamiast tego traktujesz np. Wszystkie elementy w tablicy lub wszystkie węzły w połączonej liście jako „blokadę spinową”. Czytasz, modyfikujesz i próbujesz aktualizować, jeśli od ostatniego odczytu nie było żadnej aktualizacji. Jeśli tak, spróbuj ponownie.
To sprawia, że ​​"blokowanie" (och, przepraszam, nie blokowanie :) jest bardzo drobnoziarniste, bez wprowadzania dodatkowej pamięci lub wymagań dotyczących zasobów.
Zwiększenie drobnoziarnistości zmniejsza prawdopodobieństwo oczekiwania. Zrobienie tego tak drobnoziarnistego, jak to tylko możliwe, bez wprowadzania dodatkowych wymagań dotyczących zasobów, brzmi świetnie, prawda?

Jednak największą frajdą może być zapewnienie prawidłowego ładowania / zamawiania w sklepie .
Wbrew intuicji procesory mogą dowolnie zmieniać kolejność odczytów / zapisów pamięci - nawiasem mówiąc, są bardzo sprytne: trudno będzie ci to obserwować z jednego wątku. Jednak napotkasz problemy, gdy zaczniesz wielowątkowość na wielu rdzeniach. Twoja intuicja się załamie: tylko dlatego, że instrukcja znajduje się wcześniej w kodzie, nie oznacza to, że faktycznie nastąpi to wcześniej. Procesory mogą przetwarzać instrukcje poza kolejnością: a szczególnie lubią to robić z instrukcjami z dostępem do pamięci, aby ukryć opóźnienia pamięci głównej i lepiej wykorzystać swoją pamięć podręczną.

Teraz, wbrew intuicji, jest pewne, że sekwencja kodu nie płynie „z góry na dół”, zamiast tego działa tak, jakby w ogóle nie było sekwencji - i można ją nazwać „placem zabaw diabła”. Uważam, że niemożliwe jest udzielenie dokładnej odpowiedzi na temat tego, jakie ponowne zamówienia w załadunku / sklepie będą miały miejsce. Zamiast tego, zawsze mówi w kategoriach mays i mights i puszek i przygotować się na najgorsze. „Och, procesor może zmienić kolejność tego odczytu, aby nastąpił przed zapisem, więc najlepiej jest umieścić barierę pamięci tutaj, w tym miejscu”.

Sprawy komplikuje fakt, że nawet te Mays i mights mogą różnić się w poprzek architektur procesora. To może być, na przykład, że coś, co jest gwarancją nie stało w jednej architekturze może zdarzyć się na innym.


Aby prawidłowo obsługiwać wielowątkowość bez blokad, musisz zrozumieć modele pamięci.
Uzyskanie poprawnego modelu pamięci i gwarancji nie jest jednak trywialne, jak pokazuje ta historia, w której Intel i AMD wprowadziły pewne poprawki do dokumentacji MFENCEpowodującej zamieszanie wśród programistów JVM . Jak się okazało, dokumentacja, na której deweloperzy polegali od samego początku, nie była po pierwsze tak precyzyjna.

Blokady w .NET powodują powstanie niejawnej bariery pamięci, więc możesz z nich bezpiecznie korzystać (przez większość czasu, to znaczy ... zobacz na przykład ten Joe Duffy - Brad Abrams - Vance Morrison o leniwej inicjalizacji, blokadach, ulotnościach i pamięci bariery. :) (Pamiętaj, aby skorzystać z linków na tej stronie.)

Jako dodatkowy bonus, zostaniesz wprowadzony do modelu pamięci .NET w ramach pobocznego zadania . :)

Jest też „oldie but goldie” autorstwa Vance Morrison: What Every Dev Must Know About Multithreaded Apps .

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

Dobry STM może zbliżyć się do drobnoziarnistego blokowania, jak to tylko możliwe, i prawdopodobnie zapewni wydajność, która jest zbliżona lub porównywalna z wykonaną ręcznie implementacją. Jednym z nich jest STM.NET z projektów MS DevLabs .

Jeśli nie jesteś fanatykiem tylko .NET, Doug Lea wykonał świetną robotę w JSR-166 .
Cliff Click ma interesujące podejście do tablic mieszania, które nie polega na blokowaniu pasków - jak robią to współbieżne tablice mieszania Java i .NET - i wydaje się, że dobrze skalują się do 750 procesorów.

Jeśli nie boisz się zapuszczać się na terytorium Linuksa, poniższy artykuł zawiera więcej informacji na temat wewnętrznych elementów obecnych architektur pamięci i tego, jak współdzielenie linii 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 rozważenia, łatwiejsze do wdrożenia i mniej podatne na błędy niż niedopracowana implementacja blokowania, która stara się być inteligentna. (Jest to jednak - subiektywnie - prawdziwe również w przypadku rozwiązania opartego na STM). Założę się również, że o lata świetlne łatwiej jest poprawnie napisać porządną aplikację rozproszoną np. W Erlangu, jak sugeruje wiele udanych przykładów.

MPI ma jednak swoje własne koszty i kłopoty, gdy działa na jednym, wielordzeniowym systemie . Np. W Erlang do rozwiązania są problemy związane z synchronizacją planowania procesów i kolejek wiadomości .
Ponadto, w swej istocie, systemy MPI zwykle implementują rodzaj kooperatywnego planowania N: M dla „lekkich procesów”. Oznacza to na przykład, że istnieje nieuniknione przełączanie kontekstu między lekkimi procesami. Prawdą jest, że nie jest to „klasyczny przełącznik kontekstu”, ale przeważnie operacja w przestrzeni użytkownika i można ją wykonać szybko - jednak szczerze wątpię, czy można ją sprowadzić do 20-200 cykli, jakie zajmuje operacja blokowana . Przełączanie kontekstu w trybie użytkownika jest z pewnością wolniejszenawet w bibliotece Intel McRT. Szeregowanie N: M z lekkimi procesami nie jest nowością. LWP były w Solarisie od dawna. Zostali opuszczeni. W NT były włókna. Są teraz przeważnie reliktem. W NetBSD były "aktywacje". Zostali opuszczeni. Linux miał własne podejście do tematu wątków N: M. Wydaje się, że jest już trochę martwy.
Od czasu do czasu pojawiają się nowi pretendenci: na przykład McRT firmy Intel lub ostatnio User-Mode Scheduling wraz z ConCRT firmy Microsoft.
Na najniższym poziomie robią to, co robi planista N: M MPI. Erlang - lub jakikolwiek inny system MPI - może znacznie skorzystać na systemach SMP, wykorzystując nowy UMS .

Wydaje mi się, że pytanie OP nie dotyczy zalet i subiektywnych argumentów za / przeciw jakimkolwiek rozwiązaniom, ale gdybym miał na to odpowiedzieć, wydaje mi się, że zależy to od zadania: budowy niskopoziomowych, wysokowydajnych podstawowych struktur danych, które działają na pojedynczy system 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 każdej chwili pod względem wydajności, nawet jeśli powyższe zmarszczki zostaną usunięte np. w Erlang.
Aby zbudować coś średnio bardziej złożonego, który działa w jednym systemie, być może wybrałbym klasyczne blokowanie gruboziarniste lub, jeśli wydajność ma duże znaczenie, STM.
W przypadku budowy systemu rozproszonego system MPI byłby prawdopodobnie naturalnym wyborem.
Zauważ, że istnieją również implementacje MPI dla .NET (chociaż wydają się nie być tak aktywne).


1
Chociaż ta odpowiedź zawiera wiele dobrych informacji, główna idea, że ​​algorytmy i struktury danych bez blokad są w zasadzie tylko zbiorem bardzo drobnoziarnistych spinlocków, jest błędna. Chociaż zwykle zobaczysz pętle ponawiania w strukturach bez blokad, zachowanie jest bardzo różne: blokady (w tym spinlocki) pobierają wyłącznie niektóre zasoby, a inne wątki nie mogą robić postępu, gdy są wstrzymane. W tym sensie „ponowienie” oznacza po prostu oczekiwanie na zwolnienie wyłącznego zasobu.
BeeOnRope

1
Z drugiej strony algorytmy bez blokady nie używają CAS ani innych instrukcji atomowych do uzyskania wyłącznego zasobu, ale raczej do wykonania jakiejś operacji. Jeśli zawiodą, jest to spowodowane czasowo drobnoziarnistym biegiem z inną nicią, w tym przypadku druga nić postępowała (zakończyła swoją operację). Jeśli wątek jest podejrzany w nieskończoność, wszystkie inne wątki mogą nadal działać. To bardzo różni się zarówno pod względem jakości, jak i wydajności od ekskluzywnych zamków. Liczba „ponownych prób” jest zwykle bardzo niska w przypadku większości pętli CAS, nawet przy dużej rywalizacji ...
BeeOnRope

1
... ale to oczywiście nie oznacza dobrego skalowania: rywalizacja o pojedynczą lokalizację pamięci zawsze będzie dość powolna na maszynach SMP, tylko z powodu opóźnień między rdzeniami między rdzeniami, nawet jeśli liczba błędów CAS wynosi Niska.
BeeOnRope

1
@AndrasVass - wydaje mi się, że zależy to również od „dobrego” kontra „złego” kodu bez blokady. Z pewnością każdy może napisać strukturę i nazwać ją wolną od blokad, podczas gdy tak naprawdę używa ona tylko blokady spinowej w trybie użytkownika i nawet nie spełnia definicji. Zachęcałbym również wszystkich zainteresowanych czytelników do zapoznania się z tym artykułem Herlihy and Shavit, który w formalny sposób przedstawia różne kategorie algorytmów opartych na blokadach i bez blokad. Cokolwiek Herlihy na ten temat jest również zalecane do lektury.
BeeOnRope,

1
@AndrasVass - Nie zgadzam się. Większość klasycznych struktur bez blokad (listy, kolejki, mapy współbieżne itp.) Nie obracała się nawet dla współdzielonych struktur mutowalnych, a praktyczne istniejące implementacje tego samego, na przykład w Javie, postępują według tego samego wzorca (nie jestem taki jak zaznajomiony z tym, co jest dostępne w skompilowanym natywnie C lub C ++ i jest tam trudniej ze względu na brak czyszczenia). Być może ty i ja mamy inną definicję kręcenia się: nie uważam, że "ponawianie CAS", które znajdujesz w rzeczach bez blokady, "kręci się". IMO „kręci się” oznacza gorące oczekiwanie.
BeeOnRope

27

Książka Joe Duffy'ego:

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

Prowadzi również bloga na te tematy.

Sztuczka pozwalająca uzyskać prawidłowe programy o niskim poziomie blokad polega na dokładnym zrozumieniu reguł modelu pamięci w konkretnej kombinacji sprzętu, systemu operacyjnego i środowiska wykonawczego.

Osobiście nie jestem na tyle sprytny, aby wykonać poprawne programowanie z niskim blokadą poza InterlockedIncrement, ale jeśli jesteś, świetnie, idź na to. Po prostu upewnij się, że zostawiłeś w kodzie dużo dokumentacji, aby ludzie, którzy nie są tak sprytni, jak ty, przypadkowo nie złamali jednego z niezmienników modelu pamięci i nie wprowadzili niemożliwego do znalezienia błędu.


38
Więc jeśli zarówno Eric Lippert, jak i Jon Skeet uważają, że programowanie bez blokad jest tylko dla ludzi mądrzejszych od nich samych, pokornie ucieknę z krzykiem od razu. ;-)
dodgy_coder

20

Obecnie nie ma czegoś takiego jak „wątkowanie bez blokowania”. Był to interesujący plac zabaw dla środowisk akademickich i tym podobnych 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.

Skończyły to dwa wydarzenia: rosnąca dysproporcja między szybkością pamięci RAM i procesora. I zdolność producentów chipów do umieszczenia więcej niż jednego rdzenia procesora w jednym chipie.

Problem z szybkością pamięci RAM wymagał od projektantów chipa umieszczenia bufora na chipie procesora. Bufor przechowuje kod i dane, szybko dostępne dla rdzenia procesora. I może być odczytywany i zapisywany z / do pamięci RAM w znacznie wolniejszym tempie. Ten bufor nazywany jest pamięcią podręczną procesora, 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 pierwszego poziomu, będzie działać szybko. Brak pamięci podręcznej jest naprawdę drogi, powoduje uśpienie procesora nawet na 10 cykli, jeśli dane nie znajdują się w pierwszej pamięci podręcznej, aż na 200 cykli, jeśli nie ma jej w drugiej pamięci podręcznej i należy je odczytać BARAN.

Każdy rdzeń procesora ma własną pamięć podręczną, przechowują one swój własny „widok” pamięci RAM. Kiedy procesor zapisuje dane, zapis jest wykonywany w pamięci podręcznej, która jest następnie powoli przenoszona do pamięci RAM. Nieuniknione, każdy rdzeń będzie miał teraz inny widok na zawartość pamięci RAM. Innymi słowy, jeden procesor nie wie, co zapisał inny procesor, dopóki ten cykl zapisu pamięci RAM nie zostanie zakończony, a procesor odświeży swój własny widok.

To jest dramatycznie niezgodne z wątkami. Zawsze naprawdę obchodzi cię, jaki jest stan innego wątku, gdy musisz odczytać dane, które zostały zapisane przez inny wątek. Aby to zapewnić, musisz jawnie zaprogramować tak zwaną barierę pamięci. Jest to prymitywny procesor niskiego poziomu, który zapewnia, że ​​wszystkie pamięci podręczne procesora są w spójnym stanie i mają aktualny widok pamięci RAM. Wszystkie oczekujące zapisy muszą zostać opróżnione do pamięci RAM, a następnie pamięci podręczne należy odświeżyć.

Jest to dostępne w .NET, metoda Thread.MemoryBarrier () implementuje jedną. Biorąc pod uwagę, że jest to 90% pracy, którą wykonuje instrukcja lock (i 95% czasu wykonania), po prostu nie jesteś na czele, unikając narzędzi, które daje ci .NET i próbując wdrożyć własne.


2
@ Davy8: kompozycja nadal jest trudna. Jeśli mam dwie tabele skrótów bez blokad i jako konsument mam dostęp do obu z nich, nie gwarantuje to spójności stanu jako całości. Najbliżej dostępne są dziś STM, w których dwa wejścia można umieścić np. W jednym atomicbloku. Podsumowując, konsumowanie struktur bez zamków może być w wielu przypadkach równie trudne.
Andras Vass

4
Może się mylę, ale myślę, że źle wyjaśniłeś, jak działa spójność pamięci podręcznej. Większość nowoczesnych procesorów wielordzeniowych ma spójne pamięci podręczne, co oznacza, że ​​sprzęt pamięci podręcznej zapewnia, że ​​wszystkie procesy mają ten sam widok zawartości pamięci RAM - blokując wywołania „odczytu” do momentu zakończenia wszystkich odpowiednich wywołań „zapisu”. Dokumentacja Thread.MemoryBarrier () ( msdn.microsoft.com/en-us/library/… ) w ogóle nie mówi o zachowaniu pamięci podręcznej - jest to po prostu dyrektywa, która zapobiega zmianie kolejności odczytów i zapisów przez procesor.
Brooks Moses

7
„W dzisiejszych czasach nie ma czegoś takiego jak„ wątki bez blokady ”. Powiedz to programistom Erlang i Haskell.
Juliet

4
@HansPassant: "W dzisiejszych czasach nie ma czegoś takiego jak 'wątki bez blokad'". F #, Erlang, Haskell, Cilk, OCaml, Microsoft Task Parallel Library (TPL) i Threaded Building Blocks (TBB) firmy Intel zachęcają do wielowątkowego programowania bez blokad. Obecnie rzadko używam blokad w kodzie produkcyjnym.
JD,

5
@HansPassant: "tak zwana bariera pamięci. Jest to prymitywny procesor niskiego poziomu, który zapewnia, że ​​wszystkie pamięci podręczne procesora są w spójnym stanie i mają aktualny widok pamięci RAM. Wszystkie oczekujące zapisy muszą zostać opróżnione do pamięci RAM, pamięci podręczne należy wówczas odświeżyć ”. Bariera pamięci w tym kontekście zapobiega ponownemu uporządkowaniu instrukcji pamięci (ładowań i zapisów) przez kompilator lub procesor. Nie ma to nic wspólnego ze spójnością pamięci podręcznych procesora.
JD,


0

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, to wiedza / doświadczenie, które zdobywamy podczas naszej podróży po wątkach.


Istnieje wiele bibliotek, które zapewniają semantykę obsługi wątków bez blokady. Szczególnie interesujący jest STM, którego implementacji jest sporo.
Marcelo Cantos

Widzę obie strony tego. Uzyskanie efektywnej wydajności z biblioteki bez blokad wymaga głębokiej znajomości modeli pamięci. Ale programista, który nie ma tej wiedzy, może nadal korzystać z zalet poprawności.
Ben Voigt,

0

Mimo że wątki bez blokad mogą być trudne w .NET, często można wprowadzić znaczące ulepszenia podczas korzystania z blokady, badając dokładnie, co ma być zablokowane, i minimalizując zablokowaną sekcję ... jest to również znane jako minimalizowanie ziarnistości blokady .

Na przykład powiedz po prostu, że musisz zabezpieczyć wątek kolekcji. Nie tylko na ślepo blokuj metodę iterującą po kolekcji, jeśli wykonuje ona na każdym elemencie jakieś zadanie intensywnie wykorzystujące procesor. Państwo może wystarczy umieścić blokady wokół tworząc płytkie kopię kolekcji. Iterowanie po kopii mogłoby wtedy działać bez blokady. Oczywiście jest to wysoce zależne od specyfiki twojego kodu, ale udało mi się rozwiązać problem konwoju zamków dzięki temu podejściu.

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.