Przykład / tutorial Mutex? [Zamknięte]

176

Jestem nowy w wielowątkowości i próbowałem zrozumieć, jak działają muteksy. Dużo szukałem w Google, ale nadal pozostawiało wątpliwości, jak to działa, ponieważ stworzyłem własny program, w którym blokowanie nie działało.

Jedna absolutnie nieintuicyjna składnia muteksu polega na tym pthread_mutex_lock( &mutex1 );, że wygląda na to, że mutex jest blokowany, a tym, co naprawdę chcę zablokować, jest inna zmienna. Czy ta składnia oznacza, że ​​zablokowanie muteksu blokuje region kodu, dopóki mutex nie zostanie odblokowany? Skąd więc wątki wiedzą, że region jest zablokowany? [ UPDATE: Wątki wiedzą, że region jest zamknięty przez Memory Fencing ]. A czy nie można takiego zjawiska nazwać sekcją krytyczną? [ AKTUALIZACJA: Obiekty sekcji krytycznych są dostępne tylko w systemie Windows, gdzie obiekty są szybsze niż muteksy i są widoczne tylko dla wątku, który je implementuje. W przeciwnym razie sekcja krytyczna odnosi się tylko do obszaru kodu chronionego przez mutex ]

Krótko mówiąc, czy mógłbyś pomóc w najprostszym możliwym przykładowym programie mutex i najprostszym możliwym wyjaśnieniu logiki jego działania? Jestem pewien, że pomoże to wielu innym początkującym.

Nav
źródło
2
Kontynuując podkreślanie potrzeby prostego samouczka (czy to zwiększanie wątków, tbb czy pthreads): Przykłady zamieszania: 1. stackoverflow.com/questions/3528877/… 2. stackoverflow.com/questions/2979525/… 3. stackoverflow.com/questions/2095977/to-mutex-or-not-to-mutex 4. stackoverflow.com/questions/3931026/... 5. stackoverflow.com/questions/1525189/ ...
Nawigacja
1
Nie mam na myśli tego ofensywnie, ale Twój ostatni komentarz sugeruje mi, że potrzebujemy mniej analogii i lepszego technicznego wyjaśnienia, jak działa muteks i dlaczego ich potrzebujemy.
San Jacinto
@San: Bez urazy :) Moje komentarze miały na celu jedynie zasugerowanie, że nowicjusz może uzyskać najkrótsze, najjaśniejsze wyjaśnienie muteksów. Wiele analogii może być mylących dla początkującego, więc różne analogie powinny być przechowywane oddzielnie. Cały powód, dla którego publikuję pytania i odpowiedzi, jest taki, że jako nowicjusz uważałem, że czytanie długich wyjaśnień i próbek kodu było dla mnie trudne. Nie chciałbym, żeby ktokolwiek inny przeszedł przez ból.
Nav
2
@Cory: Jeśli można by poprawić tę odpowiedź, z przyjemnością skorzystam z twoich sugestii. Cieszę się, że wiele osób uznało to za pomocne. Jeśli to ci nie pomogło, są też odpowiedzi od innych osób, które wskazały na inne samouczki muteksów. Dlaczego być tak negatywnym?
Nawigacja

Odpowiedzi:

278

Oto moja skromna próba wyjaśnienia koncepcji nowicjuszom na całym świecie: ( wersja oznaczona kolorami również na moim blogu)

Wiele osób biegnie do samotnej budki telefonicznej (nie mają telefonów komórkowych), aby porozmawiać z bliskimi. Pierwszą osobą, która złapie klamkę w budce, jest ta, która może użyć telefonu. Musi trzymać klamkę drzwi tak długo, jak używa telefonu, inaczej ktoś inny złapie za klamkę, wyrzuci go i porozmawia z żoną :) Nie ma systemu kolejek jako takiego. Kiedy osoba zakończy rozmowę, wyjdzie z budki i opuści klamkę, następna osoba, która złapie klamkę, będzie mogła używać telefonu.

Wątek jest: Każda osoba mutex wynosi: klamka blokady jest: ręka osoby zasób jest: telefon


Każdy wątek, który ma wykonać jakieś linie kodu, które nie powinny być modyfikowane przez inne wątki w tym samym czasie (używanie telefonu do rozmowy z żoną), musi najpierw uzyskać blokadę na muteksie (ściskając klamkę drzwi budki ). Dopiero wtedy wątek będzie mógł uruchomić te linie kodu (wykonując połączenie telefoniczne).

Gdy wątek wykona ten kod, powinien zwolnić blokadę muteksu, aby inny wątek mógł uzyskać blokadę muteksu (inne osoby mogą uzyskać dostęp do budki telefonicznej).

[ Koncepcja posiadania muteksu jest nieco absurdalna, biorąc pod uwagę wyłączny dostęp w świecie rzeczywistym, ale wydaje mi się, że w świecie programowania nie było innego sposobu, aby inne wątki „zobaczyły”, że wątek wykonywał już pewne linie kodu. Istnieją koncepcje rekurencyjnych muteksów itp., Ale ten przykład miał jedynie na celu pokazanie podstawowej koncepcji. Mam nadzieję, że przykład daje jasny obraz koncepcji. ]

Z wątkami C ++ 11:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Skompiluj i uruchom przy użyciu g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Zamiast jawnie używać locki unlock, możesz użyć nawiasów, jak pokazano tutaj , jeśli używasz blokady o określonym zakresie ze względu na zalety, jakie zapewnia . Zamki z lunetą mają jednak niewielki narzut wydajności.

Nav
źródło
2
@San: Będę szczery; Tak, podoba mi się fakt, że starałeś się jak najlepiej wyjaśnić szczegóły (płynnie) zupełnie nowicjuszowi. ALE, (proszę, nie zrozumcie mnie źle) intencją tego posta było zawarcie tej koncepcji w krótkim wyjaśnieniu (ponieważ inne odpowiedzi wskazywały na długie tutoriale). Mam nadzieję, że nie miałbyś nic przeciwko, gdybym poprosił Cię o skopiowanie całej odpowiedzi i opublikowanie jej jako oddzielnej odpowiedzi? Aby móc wycofać i edytować moją odpowiedź, tak aby wskazywała na Twoją odpowiedź.
Nawigacja
2
@Tom W takim przypadku nie powinieneś uzyskiwać dostępu do tego muteksu. Operacje na nim powinny być obudowane, aby cokolwiek strzeże, było chronione przed takimi głupstwami. Jeśli korzystasz z udostępnionego interfejsu API biblioteki, biblioteka gwarantuje bezpieczeństwo wątków, możesz bezpiecznie dołączyć wyraźnie inny muteks, aby chronić własne elementy udostępnione. W przeciwnym razie rzeczywiście dodajesz nową klamkę, jak sugerowałeś.
San Jacinto
2
Aby rozszerzyć mój punkt widzenia, chciałbyś dodać kolejny, większy pokój wokół budki. W pokoju może być również toaleta i prysznic. Powiedzmy, że w pomieszczeniu może przebywać tylko 1 osoba. Musisz tak zaprojektować pomieszczenie, aby to pomieszczenie miało drzwi z klamką, które chronią wejście do pomieszczenia podobnie jak budka telefoniczna. Więc teraz, nawet jeśli masz dodatkowe muteksy, możesz ponownie wykorzystać budkę telefoniczną w dowolnym projekcie. Inną opcją byłoby odsłonięcie mechanizmów blokujących dla każdego urządzenia w pokoju i zarządzanie zamkami w klasie pokoju. Tak czy inaczej, nie dodasz nowych blokad do tego samego obiektu.
San Jacinto
8
Twój przykład wątkowości w C ++ 11 jest nieprawidłowy . Tak jest z TBB, wskazówka tkwi w nazwie zamka z zakresem .
Jonathan Wakely
3
Doskonale zdaję sobie sprawę z obu, @Jonathan. Wydawało się, że przegapiłeś zdanie, które napisałem (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Jeśli chodzi o blokowanie zakresu, zależy to od programisty, w zależności od rodzaju aplikacji, którą budują. Ta odpowiedź miała na celu odniesienie się do podstawowego zrozumienia koncepcji mutexu, a nie wniknięcie we wszystkie jej zawiłości, więc twoje komentarze i linki są mile widziane, ale trochę wykraczają poza zakres tego samouczka.
Nawigacja
41

Chociaż mutex może być używany do rozwiązywania innych problemów, głównym powodem ich istnienia jest zapewnienie wzajemnego wykluczenia, a tym samym rozwiązanie tego, co jest znane jako stan rasy. Kiedy dwa (lub więcej) wątki lub procesy próbują uzyskać dostęp do tej samej zmiennej jednocześnie, mamy możliwość wystąpienia sytuacji wyścigu. Rozważmy następujący kod

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

Elementy wewnętrzne tej funkcji wyglądają tak prosto. To tylko jedno stwierdzenie. Jednak typowym odpowiednikiem języka asemblera może być:

load i from memory into a register
add 1 to i
store i back into memory

Ponieważ wszystkie równoważne instrukcje języka asemblera są wymagane do wykonania operacji inkrementacji na i, mówimy, że inkrementacja i nie jest operacją atmosferyczną. Operacja atomowa to taka, którą można zakończyć na sprzęcie z gwarancją braku przerwania po rozpoczęciu wykonywania instrukcji. Przyrost i składa się z łańcucha 3 instrukcji atomowych. W systemie współbieżnym, w którym kilka wątków wywołuje funkcję, pojawiają się problemy, gdy wątek odczytuje lub zapisuje w niewłaściwym czasie. Wyobraź sobie, że mamy dwa wątki działające jednocześnie i jeden wywołuje funkcję bezpośrednio po drugim. Powiedzmy również, że zainicjowaliśmy i na 0. Załóżmy również, że mamy dużo rejestrów i że dwa wątki używają zupełnie innych rejestrów, więc nie będzie kolizji. Rzeczywisty czas tych wydarzeń może być:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

Zdarzyło się, że mamy dwa wątki zwiększające się jednocześnie i, nasza funkcja jest wywoływana dwukrotnie, ale wynik jest niezgodny z tym faktem. Wygląda na to, że funkcja została wywołana tylko raz. Dzieje się tak, ponieważ atomowość jest „zepsuta” na poziomie maszyny, co oznacza, że ​​wątki mogą się wzajemnie przerywać lub współpracować w niewłaściwych momentach.

Potrzebujemy mechanizmu, aby rozwiązać ten problem. Musimy trochę uporządkować powyższe instrukcje. Jednym z powszechnych mechanizmów jest blokowanie wszystkich wątków oprócz jednego. Mutex Pthread wykorzystuje ten mechanizm.

Każdy wątek, który musi wykonać pewne wiersze kodu, które mogą w niebezpieczny sposób modyfikować wspólne wartości przez inne wątki w tym samym czasie (używając telefonu do rozmowy z żoną), powinien najpierw uzyskać blokadę muteksu. W ten sposób każdy wątek wymagający dostępu do udostępnionych danych musi przejść przez blokadę mutex. Dopiero wtedy wątek będzie mógł wykonać kod. Ta sekcja kodu nazywana jest sekcją krytyczną.

Gdy wątek wykona sekcję krytyczną, powinien zwolnić blokadę muteksu, aby inny wątek mógł uzyskać blokadę muteksu.

Koncepcja posiadania muteksu wydaje się nieco dziwna, gdy rozważa się ludzi poszukujących wyłącznego dostępu do rzeczywistych, fizycznych obiektów, ale podczas programowania musimy być zamierzeni. Współbieżne wątki i procesy nie mają takiego społecznego i kulturowego wychowania, jak my, dlatego musimy zmusić je do ładnego udostępniania danych.

Więc technicznie rzecz biorąc, jak działa muteks? Czy nie cierpi z powodu tych samych warunków wyścigu, o których wspominaliśmy wcześniej? Czy pthread_mutex_lock () nie jest nieco bardziej skomplikowany niż zwykły przyrost zmiennej?

Technicznie rzecz biorąc, potrzebujemy wsparcia sprzętowego, aby nam pomóc. Projektanci sprzętu przekazują nam instrukcje maszynowe, które robią więcej niż jedną rzecz, ale gwarantują, że są atomowe. Klasycznym przykładem takiej instrukcji jest test-i-ustaw (TAS). Próbując uzyskać blokadę zasobu, możemy użyć TAS może sprawdzić, czy wartość w pamięci wynosi 0. Jeśli tak, to byłby to nasz sygnał, że zasób jest używany i nic nie robimy (lub dokładniej , czekamy według jakiegoś mechanizmu. Muteks pthreads umieści nas w specjalnej kolejce w systemie operacyjnym i powiadomi nas, gdy zasób stanie się dostępny. Głupsze systemy mogą wymagać od nas wykonania ciasnej pętli spinowej, testując stan w kółko) . Jeśli wartość w pamięci nie jest równa 0, TAS ustawia lokalizację na inną niż 0 bez używania innych instrukcji. To' to jak połączenie dwóch instrukcji asemblacji w 1, aby uzyskać atomowość. Zatem testowanie i zmiana wartości (jeśli zmiana jest odpowiednia) nie może zostać przerwana po rozpoczęciu. Na podstawie takiej instrukcji możemy zbudować muteksy.

Uwaga: niektóre sekcje mogą wyglądać podobnie do wcześniejszej odpowiedzi. Przyjąłem jego zaproszenie do redagowania, wolał oryginalny sposób, więc zachowuję to, co miałem, co jest nasycone odrobiną jego słownictwa.

San Jacinto
źródło
1
Bardzo ci dziękuję, San. Podałem link do Twojej odpowiedzi :) Właściwie zamierzałem, abyś odebrał moją odpowiedź + swoją odpowiedź i opublikował ją jako oddzielną odpowiedź, aby utrzymać przepływ. Nie mam nic przeciwko, jeśli ponownie wykorzystasz jakąkolwiek część mojej odpowiedzi. I tak nie robimy tego dla siebie.
Nav
13

Najlepszy samouczek dotyczący wątków, o którym wiem, jest tutaj:

https://computing.llnl.gov/tutorials/pthreads/

Podoba mi się, że jest napisane o API, a nie o konkretnej implementacji, i podaje kilka ładnych, prostych przykładów, które pomogą ci zrozumieć synchronizację.

R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
Zgadzam się, że to zdecydowanie dobry tutorial, ale na jednej stronie jest dużo informacji, a programy są długie. Pytanie, które opublikowałem, to wersja mutex przemówienia „Mam sen”, w której początkujący znajdą prosty sposób na poznanie muteksów i zrozumienie, jak działa nieintuicyjna składnia (jest to jedno wyjaśnienie, którego brakuje we wszystkich samouczkach) .
Nav
7

Niedawno natknąłem się na ten post i myślę, że potrzebuje on zaktualizowanego rozwiązania dla muteksu c ++ 11 biblioteki standardowej (mianowicie std :: mutex).

Poniżej wkleiłem kod (pierwsze kroki z muteksem - nauczyłem się współbieżności na win32 z HANDLE, SetEvent, WaitForMultipleObjects itp.).

Ponieważ to moja pierwsza próba z std :: mutex i przyjaciółmi, chciałbym zobaczyć komentarze, sugestie i ulepszenia!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}
Dania rybne
źródło
1
Wspaniały! Dzięki za wysłanie wiadomości. Chociaż, jak wspomniałem wcześniej, moim celem było jedynie wyjaśnienie koncepcji muteksu. Wszystkie inne samouczki sprawiały, że było to bardzo trudne dzięki dodanym koncepcjom konsumenta producenta, zmiennych warunków itp., Co bardzo utrudniało mi zrozumienie, co się dzieje na ziemi.
Nav
4

Funkcja pthread_mutex_lock()albo nabywa mutex dla wątku wywołującego lub bloki gwint do mutex mogą być nabyte. Powiązane pthread_mutex_unlock()uwalnia mutex.

Pomyśl o muteksie jak o kolejce; każdy wątek, który będzie próbował uzyskać muteks, zostanie umieszczony na końcu kolejki. Kiedy wątek zwalnia muteks, następny wątek w kolejce jest odłączany i jest teraz uruchomiony.

Krytyczny punkt odnosi się do regionu kodu, w którym nie jest możliwe determinizm. Często dzieje się tak, ponieważ wiele wątków próbuje uzyskać dostęp do wspólnej zmiennej. Sekcja krytyczna nie jest bezpieczna, dopóki nie nastąpi synchronizacja. Blokada mutex jest jedną z form synchronizacji.

chrisaycock
źródło
1
Czy jest gwarantowane, że wejdzie dokładnie następny wątek próbny?
Arsen Mkrtchyan
1
@Arsen Brak gwarancji. To tylko pomocna analogia.
chrisaycock
3

Powinieneś sprawdzić zmienną mutex przed użyciem obszaru chronionego przez mutex. Więc twoja pthread_mutex_lock () może (w zależności od implementacji) czekać, aż mutex1 zostanie zwolniony lub zwrócić wartość wskazującą, że nie można było uzyskać blokady, jeśli ktoś inny już ją zablokował.

Mutex to tak naprawdę uproszczony semafor. Jeśli o nich czytasz i je rozumiesz, rozumiesz muteksy. Istnieje kilka pytań dotyczących muteksów i semaforów w SO. Różnica między binarnym semaforem a muteksem , kiedy powinniśmy używać muteksu, a kiedy semafora i tak dalej. Przykład toalety w pierwszym linku jest tak dobrym przykładem, jak tylko można sobie wyobrazić. Cały kod służy do sprawdzenia, czy klucz jest dostępny, a jeśli jest, rezerwuje go. Zwróć uwagę, że tak naprawdę nie rezerwujesz samej toalety, ale klucz.

Makis
źródło
1
pthread_mutex_locknie może wrócić, jeśli ktoś inny przytrzyma zamek. W tym przypadku blokuje i o to chodzi. pthread_mutex_trylockto funkcja, która powróci po przytrzymaniu blokady.
R .. GitHub PRZESTAŃ POMÓC NA LODZIE
1
Tak, na początku nie zdawałem sobie sprawy, co to za realizacja.
Makis,
3

Dla tych, którzy szukają przykładu shortex mutex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}
Unieważnić
źródło