Blokada rekurencyjna (Mutex) vs Blokada nierekurencyjna (Mutex)

183

POSIX pozwala rekursywnym muteksom. Oznacza to, że ten sam wątek może dwa razy zablokować ten sam muteks i nie zablokuje się. Oczywiście musi również odblokować go dwa razy, w przeciwnym razie żaden inny wątek nie może uzyskać muteksu. Nie wszystkie systemy obsługujące pthreads również obsługują rekurencyjne muteksy, ale jeśli chcą być zgodne z POSIX, muszą to zrobić .

Inne interfejsy API (bardziej zaawansowane interfejsy API) również zwykle oferują muteksy, często nazywane Blokadami. Niektóre systemy / języki (np. Cocoa Objective-C) oferują zarówno rekurencyjne, jak i nierekurencyjne muteksy. Niektóre języki oferują tylko jeden lub drugi. Np. W Javie muteksy są zawsze rekurencyjne (ten sam wątek może dwukrotnie „synchronizować” na tym samym obiekcie). W zależności od tego, jakie inne funkcje wątku oferują, brak rekurencyjnych muteksów może nie stanowić problemu, ponieważ można je łatwo napisać samodzielnie (sam już zaimplementowałem rekurencyjne muteksy na podstawie prostszych operacji mutex / warunek).

Czego tak naprawdę nie rozumiem: Do czego służą nierekurencyjne muteksy? Dlaczego miałbym chcieć zablokować wątek, jeśli dwukrotnie blokuje ten sam muteks? Nawet języki wysokiego poziomu, które mogłyby tego uniknąć (np. Sprawdzanie, czy to się zakleszczy i zgłaszanie wyjątku, jeśli tak się dzieje) zwykle tego nie robią. Zamiast tego pozwolą na impas w wątku.

Czy dotyczy to tylko przypadków, w których przypadkowo blokuję go dwukrotnie i odblokowuję tylko raz, a w przypadku rekurencyjnego muteksu trudniej byłoby znaleźć problem, więc zamiast tego mam natychmiastową impas, aby zobaczyć, gdzie pojawia się nieprawidłowa blokada? Ale czy nie mogę zrobić tego samego, gdy licznik zamków jest zwracany podczas odblokowywania iw sytuacji, gdy jestem pewien, że zwolniłem ostatnią blokadę, a licznik nie jest równy zero, mogę zgłosić wyjątek lub zarejestrować problem? Czy jest jakiś inny, bardziej użyteczny przypadek użycia nierekurencyjnych muteksów, których nie widzę? A może to tylko wydajność, ponieważ nierekurencyjny muteks może być nieco szybszy niż rekursywny? Jednak przetestowałem to i różnica nie jest tak duża.

Mecki
źródło

Odpowiedzi:

154

Różnica między rekursywnym i nierekurencyjnym muteksem ma związek z własnością. W przypadku rekurencyjnego muteksu jądro musi śledzić wątek, który faktycznie uzyskał muteks za pierwszym razem, aby mógł wykryć różnicę między rekurencją a innym wątkiem, który powinien zamiast tego zablokować. Jak wskazano w innej odpowiedzi, pojawia się pytanie o dodatkowe koszty związane z pamięcią do przechowywania tego kontekstu, a także o cykle wymagane do jego utrzymania.

W grę wchodzą jednak także inne względy.

Ponieważ rekurencyjny muteks ma poczucie własności, wątek, który chwyta muteks, musi być tym samym wątkiem, który uwalnia mutex. W przypadku nierekurencyjnych muteksów nie ma poczucia własności i każdy wątek może zwykle uwolnić muteks bez względu na to, który wątek pierwotnie wziął muteks. W wielu przypadkach ten rodzaj „muteksu” jest w rzeczywistości bardziej działaniem semaforowym, w którym niekoniecznie używasz muteksu jako urządzenia wykluczającego, ale używasz go jako urządzenia synchronizującego lub sygnalizującego między dwoma lub więcej wątkami.

Inną właściwością, która ma poczucie własności w muteksie, jest możliwość wspierania dziedziczenia priorytetowego. Ponieważ jądro może śledzić wątek będący właścicielem muteksu, a także tożsamość wszystkich programów blokujących, w systemie z priorytetowym wątkiem możliwe jest zwiększenie priorytetu wątku, który obecnie jest właścicielem muteksu, do priorytetu wątku o najwyższym priorytecie który obecnie blokuje muteks. Dziedziczenie zapobiega problemowi inwersji priorytetów, który może wystąpić w takich przypadkach. (Pamiętaj, że nie wszystkie systemy obsługują dziedziczenie priorytetowe w takich muteksach, ale jest to kolejna funkcja, która staje się możliwa dzięki pojęciu własności).

Jeśli odwołujesz się do klasycznego jądra VxWorks RTOS, definiują one trzy mechanizmy:

  • mutex - obsługuje rekurencję i opcjonalnie dziedziczenie priorytetowe. Ten mechanizm jest powszechnie stosowany do spójnej ochrony krytycznych części danych.
  • semafor binarny - bez rekurencji, bez dziedziczenia, zwykłe wykluczenie, biorca i dawca nie musi być tym samym wątkiem, dostępna wersja transmisji. Ten mechanizm może być używany do ochrony krytycznych sekcji, ale jest również szczególnie przydatny do spójnej sygnalizacji lub synchronizacji między wątkami.
  • liczenie semaforów - brak rekurencji lub dziedziczenia, działa jak spójny licznik zasobów od dowolnej pożądanej początkowej liczby, blokuje tylko wątki, w których liczba netto względem zasobu wynosi zero.

Ponownie, różni się to nieco w zależności od platformy - szczególnie jak nazywają te rzeczy, ale powinno to być reprezentatywne dla pojęć i różnych mechanizmów w grze.

Wysoki Jeff
źródło
9
twoje wyjaśnienie dotyczące nierekurencyjnego muteksu brzmiało bardziej jak semafor. Muteks (rekurencyjny lub nierekurencyjny) ma pojęcie własności.
Jay D
@JayD To bardzo mylące, gdy ludzie kłócą się o takie rzeczy .. więc kto jest tym podmiotem, który je definiuje?
Pacerier,
13
@Pacerier Odpowiedni standard. Ta odpowiedź jest niepoprawna np. W przypadku POSIX-a (pthreads), gdzie odblokowanie normalnego muteksu w wątku innym niż wątek, który go zablokował, jest niezdefiniowanym zachowaniem, przy jednoczesnym sprawdzeniu błędu lub rekursywnym muteksie daje przewidywalny kod błędu. Inne systemy i standardy mogą zachowywać się zupełnie inaczej.
nos
Być może jest to naiwne, ale miałem wrażenie, że główną ideą muteksu jest to, że nić blokująca odblokowuje muteks, a następnie inne wątki mogą zrobić to samo. From computing.llnl.gov/tutorials/pthreads :
user657862
2
@curiousguy - wydanie rozgłoszeniowe uwalnia wszystkie wątki zablokowane w semaforze bez wyraźnego podania (pozostaje puste), podczas gdy normalne podanie binarne zwalnia wątek na początku kolejki oczekującej (zakładając, że jest zablokowany).
Wysoki Jeff
123

Odpowiedź to nie wydajność. Muteksy niezwiązane z rejestracją prowadzą do lepszego kodu.

Przykład: A :: foo () przejmuje blokadę. Następnie wywołuje B :: bar (). To działało dobrze, kiedy to napisałeś. Ale jakiś czas później ktoś zmienia B :: bar (), aby wywołać A :: baz (), który również uzyskuje blokadę.

Cóż, jeśli nie masz rekurencyjnych muteksów, to impas. Jeśli je masz, działa, ale może się zepsuć. A :: foo () mógł pozostawić obiekt w niespójnym stanie przed wywołaniem bar (), przy założeniu, że baz () nie może zostać uruchomiony, ponieważ również nabywa muteks. Ale to chyba nie powinno działać! Osoba, która napisała A :: foo () założyła, że ​​nikt nie może wywołać A :: baz () w tym samym czasie - to jest cały powód, dla którego obie metody uzyskały blokadę.

Właściwy model mentalny do używania muteksów: Muteks chroni niezmiennika. Gdy muteks jest trzymany, niezmiennik może się zmienić, ale przed zwolnieniem muteksu niezmiennik zostaje ponownie ustanowiony. Blokady ponownego wysyłania są niebezpieczne, ponieważ przy drugim zakupie blokady nie możesz być już pewien, że niezmiennik jest prawdziwy.

Jeśli jesteś zadowolony z blokad ponownego wysyłania, to tylko dlatego, że nie musiałeś wcześniej debugować takiego problemu. Nawiasem mówiąc, Java ma obecnie blokadę bez ponownej rejestracji w java.util.concurrent.locks.

Jonathan
źródło
4
Zajęło mi trochę czasu, zanim zrozumiałem, co mówisz o tym, że niezmiennik nie jest ważny, gdy chwytasz zamek po raz drugi. Słuszna uwaga! Co jeśli byłby to blokada do odczytu i zapisu (jak Java ReadWriteLock), a ty nabyłeś blokadę odczytu, a następnie ponownie nabyłeś blokadę odczytu w tym samym wątku. Nie unieważniłbyś niezmiennika po uzyskaniu blokady odczytu, prawda? Kiedy więc zdobędziesz drugą blokadę odczytu, niezmiennik jest nadal prawdziwy.
dgrant
1
@Jonathan Czy Java ma obecnie blokadę bez ponownego dostępu w java.util.concurrent.locks ?
user454322
1
+1 Wydaje mi się, że najczęstszym zastosowaniem blokady dostępu jest jedna klasa, w której niektóre metody mogą być wywoływane zarówno z elementów strzeżonych, jak i niestrzeżonych. Można to faktycznie zawsze uwzględnić. @ user454322 Jasne Semaphore.
maaartinus
1
Przepraszam za moje nieporozumienie, ale nie rozumiem, jak to ma znaczenie dla muteksu. Załóżmy, że nie dotyczy to wielowątkowości i blokowania, a A::foo()przed wywołaniem obiekt mógł pozostawić niespójny stan A::bar(). Co muteks, rekurencyjny czy nie, ma coś wspólnego z tą sprawą?
Siyuan Ren,
1
@SiyuanRen: Problem polega na możliwości lokalnego uzasadnienia kodu. Ludzie (przynajmniej ja) są szkoleni, aby rozpoznawać zablokowane regiony jako utrzymanie niezmienne, to znaczy w momencie zdobycia blokady żaden inny wątek nie modyfikuje stanu, więc niezmienniki w obszarze krytycznym pozostają w mocy. Nie jest to trudna reguła i możesz kodować z pominięciem niezmienników, ale tylko utrudni to zrozumienie i utrzymanie kodu. To samo dzieje się w trybie jednowątkowym bez muteksów, ale tam nie jesteśmy szkoleni do lokalnego rozumowania wokół chronionego regionu.
David Rodríguez - dribeas
92

Jak napisał sam Dave Butenhof :

„Największy ze wszystkich dużych problemów z rekurencyjnymi muteksami polega na tym, że zachęcają cię do całkowitego zejścia z planu blokowania i zakresu. To jest śmiertelne. Zło. To„ zjadacz nici ”. Trzymasz zamki przez możliwie najkrótszy czas. Okres. Zawsze. Jeśli dzwonisz z blokadą trzymaną tylko dlatego, że nie wiesz, czy blokada jest zablokowana, lub ponieważ nie wiesz, czy odbiorca potrzebuje muteksu, to trzymasz go zbyt długo. celowanie strzelbą w aplikację i pociągnięcie za spust. Prawdopodobnie zacząłeś używać wątków, aby uzyskać współbieżność, ale właśnie ZAPOBIEGAŁeś współbieżności. ”

Chris Cleeland
źródło
9
Zwróć też uwagę na ostatnią część odpowiedzi ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
Butenhofa
2
Mówi również, że użycie jednego globalnego rekurencyjnego muteksu (jego zdaniem jest to, że potrzebujesz tylko jednego) jest w porządku jako kula do świadomego odkładania ciężkiej pracy nad zrozumieniem niezmienności zewnętrznej biblioteki, gdy zaczniesz używać jej w kodzie wielowątkowym. Ale nie powinieneś używać kul na zawsze, ale w końcu poświęć czas na zrozumienie i naprawienie niezmienników współbieżności kodu. Możemy więc sparafrazować, że używanie rekurencyjnego muteksu jest długiem technicznym.
FooF,
13

Właściwy model mentalny do używania muteksów: Muteks chroni niezmiennika.

Dlaczego jesteś pewien, że to naprawdę właściwy model mentalny do używania muteksów? Myślę, że odpowiedni model chroni dane, ale nie niezmienniki.

Problem ochrony niezmienników występuje nawet w aplikacjach jednowątkowych i nie ma nic wspólnego z wielowątkowością i muteksami.

Ponadto, jeśli chcesz chronić niezmienniki, nadal możesz użyć binarnego semafora, który nigdy nie jest rekurencyjny.


źródło
Prawdziwe. Istnieją lepsze mechanizmy ochrony niezmiennika.
ActiveTrayPrntrTagDataStrDrvr
8
To powinien być komentarz do odpowiedzi, która zawierała to stwierdzenie. Muteksy nie tylko chronią dane, ale także niezmienniki. Spróbuj napisać prosty kontener (najprostszy stos) pod względem atomowym (gdzie dane się chronią) zamiast muteksów, a zrozumiesz instrukcję.
David Rodríguez - dribeas
Muteksy nie chronią danych, chronią niezmiennika. Tego niezmiennika można jednak użyć do ochrony danych.
Jon Hanna
4

Jednym z głównych powodów, dla których rekurencyjne muteksy są przydatne, jest wielokrotne uzyskiwanie dostępu do metod przez ten sam wątek. Powiedzmy na przykład, że jeśli blokada mutex chroni bank A / c przed wypłatą, to jeśli z wypłatą wiąże się również opłata, należy zastosować ten sam mutex.

avis
źródło
4

Jedynym dobrym przypadkiem użycia muteksu rekurencyjnego jest sytuacja, gdy obiekt zawiera wiele metod. Gdy dowolna z metod modyfikuje zawartość obiektu, a zatem musi zablokować obiekt, zanim stan będzie znów spójny.

Jeśli metody używają innych metod (np .: addNewArray () wywołuje metodę addNewPoint () i kończy się poleceniem recheckBounds ()), ale każda z tych funkcji musi sama zablokować muteks, rekurencyjny muteks jest korzystny dla wszystkich.

W każdym innym przypadku (rozwiązanie po prostu złego kodowania, użycie go nawet w różnych obiektach) jest wyraźnie błędne!

DarkZeros
źródło
1

Do czego służą nierekurencyjne muteksy?

Są absolutnie dobre, gdy musisz upewnić się, że mutex jest odblokowany przed zrobieniem czegoś. Jest tak, ponieważ pthread_mutex_unlockmoże zagwarantować, że muteks zostanie odblokowany tylko wtedy, gdy nie będzie rekurencyjny.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Jeśli g_mutexnie jest rekurencyjny, powyższy kod gwarantuje wywołanie bar()przy odblokowanym muteksie .

W ten sposób eliminuje się możliwość zakleszczenia w przypadku bar(), gdy zdarza się, że jest to nieznana funkcja zewnętrzna, która może zrobić coś, co może spowodować, że inny wątek będzie próbował uzyskać ten sam muteks. Takie scenariusze nie są rzadkie w aplikacjach zbudowanych na pulach wątków oraz w aplikacjach rozproszonych, w których wywołanie międzyprocesowe może odrodzić nowy wątek, nawet jeśli programista klienta nawet tego nie zauważy. We wszystkich takich scenariuszach najlepiej jest wywoływać wspomniane funkcje zewnętrzne dopiero po zwolnieniu blokady.

Gdyby g_mutexbyło rekurencyjne, po prostu nie byłoby sposobu, aby upewnić się, że jest odblokowane przed wykonaniem połączenia.

Igor G.
źródło