Przykład / tutorial Mutex?

Jestem nowy w wielowątkowości i próbowałem zrozumieć, jak działają mutexy. Dużo Wygooglowałem i znalazłem porządny tutorial , ale nadal pozostawiał pewne wątpliwości, jak to działa, ponieważ stworzyłem własny program, w którym blokowanie nie działało.

Jedną absolutnie nieintuicyjną składnią mutex jest pthread_mutex_lock( &mutex1 );, gdzie wygląda na to, że mutex jest blokowany, kiedy to, co naprawdę chcę zablokować, to jakaś inna zmienna. Czy ta składnia oznacza, że blokowanie mutex blokuje region kodu dopóki mutex nie zostanie odblokowany? Więc skąd wątki wiedzą, że region jest zablokowany? [UPDATE: Threads know that the region is locked, by Ogrodzenie pamięci ]. A czy takie zjawisko nie powinno się nazywać sekcją krytyczną? [UPDATE: Obiekty sekcji krytycznej są dostępne tylko w systemie Windows, gdzie obiekty są szybsze niż mutexy 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 ]

W skrócie, czy mógłbyś pomóc w najprostszym możliwym programie przykładowym mutex i najprostszym możliwym wyjaśnieniu na temat logiki jego działania? Jestem pewien, że to pomoże plenty innych nowicjuszy.

Author: Veedrac, 2011-02-14

8 answers

Oto moja skromna próba wyjaśnienia tej koncepcji początkującym na całym świecie: (a wersja kodowana kolorami również na moim blogu)

Wiele osób biegnie do Samotnej budki telefonicznej (bez telefonów komórkowych), aby porozmawiać z bliskimi. Pierwszą osobą, która złapie klamkę do kabiny, jest ten, kto może korzystać z telefonu. Musi trzymać się klamki drzwi tak długo, jak używa telefonu, w przeciwnym razie ktoś inny złapie klamkę, wyrzuci go i porozmawiaj z żoną :) nie ma systemu kolejkowego jako takiego. Kiedy osoba zakończy połączenie, wyjdzie z kabiny i opuści klamkę, następna osoba, która chwyci klamkę, będzie mogła korzystać z telefonu.

A wątek to: każda osoba
mutex to: klamka do drzwi
Zamek to: ręka osoby
Źródło to: telefon

Każdy wątek, który musi wykonać kilka linii kodu, które nie powinny być modyfikowane przez inne wątki w tym samym czasie(za pomocą telefonu do rozmowy z żoną), musi najpierw nabyć Zamek na mutex (ściskając klamkę kabiny). Tylko 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ę w mutex, aby inny wątek mógł uzyskać blokadę w mutex (inne osoby mogą uzyskać dostęp do budki telefonicznej).

[pojęcie posiadanie mutex-a jest nieco absurdalne, jeśli chodzi o rzeczywisty, ekskluzywny dostęp, ale w świecie programowania nie było innego sposobu, aby pozwolić innym wątkom 'zobaczyć', że wątek już wykonuje niektóre linie kodu. Istnieją koncepcje rekurencyjnych muteksów itp., ale ten przykład miał tylko pokazać podstawowe pojęcie. Mam nadzieję, że przykład daje jasny obraz koncepcji.]

Z C++11 threading:

#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;
}

Kompilować i uruchamiać za pomocą g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Zamiast jawnie używać lock i unlock, możesz użyć nawiasów , Jak pokazano tutaj , Jeśli używasz blokady zakresowej dla korzyści, jakie daje. Zamki Scoped mają jednak niewielką wydajność nad głową.

Z TBB: Musisz TBB aby uruchomić poniższy program, ale intencją opublikowania kodu TBB jest to, że rozumiesz sekwencję blokowania i odblokowywania po prostu patrząc na prosty kod (mogłeś pokazać blokowanie zasięgu przez nie użycie acquire and release - , które również jest bezpieczne dla WYJĄTKÓW -, ale jest to jaśniejsze).

#include <iostream>
#include "/tbb/mutex.h"
#include "/tbb/tbb_thread.h"
using namespace tbb;

typedef mutex myMutex;
static myMutex sm;
int i = 0;

void someFunction() 
{ 
      //Note: Since a scoped lock is used below, you should know that you 
      //can specify a scope for the mutex using curly brackets, instead of 
      //using lock.acquire() and lock.release(). The lock will automatically 
      //get released when program control goes beyond the scope.
      myMutex::scoped_lock lock;//create a lock
      lock.acquire(sm);//Method acquire waits until it can acquire a lock on the mutex
         //***only one thread can access the lines from here...***
         ++i;//incrementing i is safe (only one thread can execute the code in this scope) because the mutex locked above protects all lines of code until the lock release.
         sleep(1);//simply creating a delay to show that no other thread can increment i until release() is executed
         std::cout<<"In someFunction "<<i<<"\n";
         //***...to here***
      lock.release();//releases the lock (duh!)      
}

int main()
{
   tbb_thread my_thread1(someFunction);//create a thread which executes 'someFunction'
   tbb_thread my_thread2(someFunction);
   tbb_thread my_thread3(someFunction);

   my_thread1.join();//This command causes the main thread (which is the 'calling-thread' in this case) to wait until thread1 completes its task.
   my_thread2.join();
   my_thread3.join();
}

Zauważ, że {[5] } jest przestarzały. Zamiennik jest pokazany tutaj .

 222
Author: Nav,
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
2018-05-05 15:24:10

Podczas gdy 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 warunek rasy. Gdy dwa (lub więcej) wątki lub procesy próbują uzyskać dostęp do tej samej zmiennej jednocześnie, mamy potencjał dla warunku race. Rozważ następujący kod

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

Wewnętrzne funkcje tej funkcji wyglądają tak prosto. To tylko jedno stwierdzenie. Jednak typowy odpowiednik języka pseudo-assembly może be:

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

Ponieważ równoważne instrukcje języka asemblera są wymagane do wykonania operacji inkrementacji na i, mówimy, że inkrementacja i jest operacją bez atmoic. Operacja atomowa to operacja, która może być zakończona na sprzęcie z gwarancją, że nie zostanie przerwana po rozpoczęciu wykonywania instrukcji. Inkrementacja i składa się z łańcucha 3 atomowych instrukcji. W systemie równoległym, w którym kilka wątków wywołuje funkcję, pojawiają się problemy, gdy wątek czyta lub pisze w niewłaściwym czasie. Wyobraźmy sobie, że mamy dwa wątki działające jednocześnie i jeden wywołuje funkcję zaraz po drugim. Załóżmy również, że zainicjowaliśmy 0. Załóżmy również, że mamy mnóstwo rejestrów i że dwa wątki używają zupełnie różnych rejestrów, więc nie będzie kolizji. Rzeczywisty czas tych zdarzeń może wynosić:

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

Stało się to, że mamy dwa wątki zwiększające i jednocześnie, 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 dlatego, że atomiczność jest "zepsuta" na poziomie maszyny, co oznacza, że wątki mogą się wzajemnie przerywać lub współpracować w niewłaściwym czasie.

Potrzebujemy mechanizmu, by to rozwiązać. Musimy nałożyć pewne rozkazy do powyższych instrukcji. Jednym z powszechnych mechanizmów jest blokowanie wszystkich wątków z wyjątkiem jednego. Pthread mutex wykorzystuje ten mechanizm.

Każdy wątek, który musi wykonać kilka linii kodu które mogą w tym samym czasie modyfikować wartości współdzielone przez inne wątki (używając telefonu do rozmowy z żoną), należy najpierw wykonać blokadę na mutexie. W ten sposób każdy wątek, który wymaga dostępu do udostępnionych danych, musi przejść przez blokadę mutex. Tylko 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ę mutex, aby inny wątek mógł namierzyć mutex.

Koncepcja posiadania mutexu wydaje się nieco dziwna, gdy rozważa się ludzi szukają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ą społecznego i kulturowego wychowania, które robimy, więc musimy zmusić je do dzielenia się danymi ładnie. Więc technicznie rzecz biorąc, jak działa mutex? Czy nie cierpią z powodu tych samych warunków rasowych, o których wspominaliśmy wcześniej? Nie jest pthread_mutex_lock () trochę bardziej złożony niż prosty przyrost zmiennej? Technicznie rzecz biorąc, potrzebujemy wsparcia sprzętowego, aby nam pomóc. Projektanci sprzętu dają nam instrukcje maszynowe, które robią więcej niż jedną rzecz, ale gwarantują, że będą atomowe. Klasycznym przykładem takiej instrukcji jest test-and-set (tas). Podczas próby uzyskania blokady zasobu, możemy użyć TAS może sprawdzić, czy wartość w pamięci jest 0. Jeśli tak, to byłby to nasz sygnał, że zasób jest w użyciu i nic nie robimy (a dokładniej, czekamy na jakiś mechanizm. Pthreads mutex 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 zrobienia ciasnej pętli wirowania, testowania stanu w kółko). Jeśli wartość w pamięci nie jest równa 0, TAS ustawia lokalizację na inną niż 0 bez użycia innych instrukcji. To tak, jakby połączyć dwie instrukcje montażu w jedną, aby dać nam atomicity. Tak więc Testowanie i zmiana wartości (jeśli zmiana jest odpowiednia) nie może zostać przerwana po jej rozpoczęciu. Na podstawie takiej instrukcji możemy zbudować mutexy.

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

 31
Author: San Jacinto,
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
2011-03-01 20:56:28

The best threads tutorial I know of is here:

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

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

 11
Author: R..,
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
2011-02-14 09:06:44

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

Wkleiłem trochę kodu poniżej (moje pierwsze kroki z mutex-nauczyłem się współbieżności na win32 z HANDLE, SetEvent, WaitForMultipleObjects itp).

Ponieważ jest 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;
}
 7
Author: fishfood,
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-11-12 23:20:11

Funkcja pthread_mutex_lock()albo przejmuje mutex dla wątku wywołującego lub blokuje wątek, dopóki nie zostanie pozyskany mutex. Related pthread_mutex_unlock() uwalnia mutex.

Myśl o mutexie jak o kolejce; każdy wątek, który próbuje zdobyć mutex, będzie umieszczany na końcu kolejki. Gdy wątek zwalnia mutex, następny wątek w kolejce wyłącza się i jest uruchomiony.

A sekcja krytyczna odnosi się do regionu kodu, w którym niedeterminizm jest możliwe. Często dzieje się tak, ponieważ wiele wątków próbuje uzyskać dostęp do współdzielonej zmiennej. Sekcja krytyczna nie jest Bezpieczna, dopóki nie zostanie wprowadzona jakaś synchronizacja. Blokada mutex jest jedną z form synchronizacji.

 4
Author: chrisaycock,
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
2011-02-14 06:44:00

Powinieneś sprawdzić zmienną mutex przed użyciem obszaru chronionego przez mutex. Zatem pthread_mutex_lock () może (w zależności od implementacji) poczekać na zwolnienie mutex1 lub zwrócić wartość wskazującą, że blokada nie może zostać uzyskana, jeśli ktoś inny ją już zablokował.

Mutex jest tak naprawdę tylko uproszczonym semaforem. Jeśli czytasz o nich i je rozumiesz, rozumiesz muteksy. Istnieje kilka pytań dotyczących muteksów i semaforów w SO. różnica między semaforem binarnym a mutexem, kiedy powinniśmy używać mutex i kiedy powinniśmy używać semafora i tak dalej. Przykład toalety w pierwszym linku jest tak dobrym przykładem, jak można myśleć. Wszystko, co robi kod, to sprawdzanie, czy klucz jest dostępny, a jeśli jest, rezerwuje go. Zauważ, że tak naprawdę nie zarezerwować samej toalety, ale klucz.

 3
Author: Makis,
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:03:09

SEMAFOR PRZYKŁAD ::

sem_t m;
sem_init(&m, 0, 0); // initialize semaphore to 0

sem_wait(&m);
// critical section here
sem_post(&m);

Odniesienie: http://pages.cs.wisc.edu / ~remzi/Classes/537/Fall2008/Notes/threads-semaphores.txt

 2
Author: parasrish,
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
2015-08-03 17:56:00

Dla szukających shortex mutex przykład:

#include <mutex>
using namespace std;

int main() {
    mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();

    return 0;
}
 0
Author: nathangeorge1,
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
2018-07-24 15:26:03