std :: unique_lock <std :: mutex> czy std :: lock_guard <std :: mutex>?

349

Mam dwa przypadki użycia.

A. Chcę zsynchronizować dostęp przez dwa wątki do kolejki.

B. Chcę zsynchronizować dostęp do kolejki przez dwa wątki i użyć zmiennej warunkowej, ponieważ jeden z wątków będzie oczekiwał na zawartość zapisaną w kolejce przez drugi wątek.

Dla przypadku użycia AI patrz przykładowy kod za pomocą std::lock_guard<>. W przypadku użycia BI patrz przykład kodu przy użyciu std::unique_lock<>.

Jaka jest różnica między tymi dwoma i które należy użyć w którym przypadku użycia?

chmike
źródło

Odpowiedzi:

344

Różnica polega na tym, że możesz zablokować i odblokować std::unique_lock. std::lock_guardzostaną zablokowane tylko raz na budowie i odblokowane po zniszczeniu.

Tak więc dla przypadku użycia B zdecydowanie potrzebujesz std::unique_lockzmiennej warunkowej. W przypadku A zależy, czy musisz ponownie zablokować strażnika.

std::unique_lockma inne cechy, które pozwalają mu np .: być budowanym bez natychmiastowego blokowania muteksu, ale budować opakowanie RAII (patrz tutaj ).

std::lock_guardzapewnia również wygodne opakowanie RAII, ale nie może bezpiecznie zablokować wielu muteksów. Może być używany, gdy potrzebujesz otoki dla ograniczonego zakresu, np .: funkcja członka:

class MyClass{
    std::mutex my_mutex;
    void member_foo() {
        std::lock_guard<mutex_type> lock(this->my_mutex);            
        /*
         block of code which needs mutual exclusion (e.g. open the same 
         file in multiple threads).
        */

        //mutex is automatically released when lock goes out of scope           
};

Aby wyjaśnić tę sprawę w drodze chmike, domyślnie std::lock_guardi std::unique_locksą takie same. Tak więc w powyższym przypadku można zastąpić std::lock_guardz std::unique_lock. Jednak std::unique_lockmoże mieć nieco więcej kosztów ogólnych.

Zauważ, że w dzisiejszych czasach należy używać std::scoped_lockzamiast std::lock_guard.

Stephan Dollberg
źródło
2
Z instrukcją std :: unique_lock <std :: mutex> lock (myMutex); czy muteks zostanie zablokowany przez konstruktora?
chmike
3
@chmike Tak, będzie. Dodano wyjaśnienie.
Stephan Dollberg,
10
@chmike Cóż, myślę, że mniej chodzi o wydajność niż o funkcjonalność. Jeśli std::lock_guardwystarczy dla twojego przypadku A, powinieneś go użyć. Nie tylko pozwala uniknąć niepotrzebnego obciążenia, ale także pokazuje czytelnikowi zamiar, że nigdy nie odblokujesz tej osłony.
Stephan Dollberg,
5
@chmike: Teoretycznie tak. Jednak Mutices nie są dokładnie lekkimi konstrukcjami, więc dodatkowy koszt unique_lockprawdopodobnie zmaleje przez koszt faktycznego zablokowania i odblokowania muteksu (jeśli kompilator nie zoptymalizuje tego narzutu, co może być możliwe).
Grizzly,
6
So for usecase B you definitely need a std::unique_lock for the condition variable- tak, ale tylko w tym wątku cv.wait(), ponieważ ta metoda atomowo uwalnia muteks. W drugim wątku, w którym aktualizujesz zmienne współdzielone, a następnie wywołujesz cv.notify_one(), lock_guardwystarczy zablokować muteks w zasięgu ... chyba że robisz coś bardziej skomplikowanego, czego nie wyobrażam sobie! np. en.cppreference.com/w/cpp/thread/condition_variable - działa dla mnie :)
underscore_d
115

lock_guardi unique_locksą prawie tym samym; lock_guardjest ograniczoną wersją z ograniczonym interfejsem.

lock_guardZawsze posiada zamek od jego budowy do jej zniszczenia. unique_lockMogą być tworzone bez natychmiastowego blokowania, można odblokować w dowolnym momencie swojego istnienia i może przenieść własność zamka z jednej instancji do drugiej.

Więc zawsze korzystasz lock_guard, chyba że potrzebujesz możliwości unique_lock. condition_variablePotrzebuje unique_lock.

Sebastian Redl
źródło
11
A condition_variable needs a unique_lock.- tak, ale tylko po wait()stronie ing, jak rozwinięto w moim komentarzu do inf.
underscore_d
48

Używaj, lock_guardchyba że musisz ręcznie unlockmuteksować bez niszczenia lock.

W szczególności condition_variableodblokowuje muteks, gdy idziesz spać po wezwaniach do wait. Dlatego a lock_guardnie jest tutaj wystarczające.

ComicSansMS
źródło
Przekazywanie blokady_zablokowania do jednej z metod oczekiwania zmiennej warunkowej byłoby w porządku, ponieważ mutex jest zawsze odzyskiwany po zakończeniu oczekiwania, z jakiegokolwiek powodu. Jednak standard zapewnia tylko interfejs dla unikalnego_bloku. Można to uznać za wadę standardu.
Chris Vine
3
@Chris W tym przypadku nadal zerwałbyś enkapsulację. Metoda oczekiwania musiałaby być w stanie wyodrębnić muteks z lock_guardi odblokować go, tym samym tymczasowo przerywając niezmiennik klasy wartownika. Mimo że zdarza się to niewidoczne dla użytkownika, uważam, że jest to uzasadniony powód, dla którego nie zezwala się na korzystanie lock_guardw tym przypadku.
ComicSansMS,
Jeśli tak, byłoby to niewidoczne i niewykrywalne. robi to gcc-4.8. czekaj (unikalny_w <muteks> &) wywołuje __gthread_cond_wait (& _ M_cond, __lock.mutex () -> native_handle ()) (patrz libstdc ++ - v3 / src / c ++ 11 / condition_variable.cc), który wywołuje pthread_cond_wait () (patrz libgcc /gthr-posix.h). To samo można zrobić dla lock_guard (ale nie dlatego, że nie jest to standard dla zmiennej_warunkowej).
Chris Vine
4
@Chris Chodzi o to, że w lock_guardogóle nie pozwala na odzyskanie bazowego muteksu. Jest to celowe ograniczenie, aby umożliwić prostsze rozumowanie na temat kodu, który używa, lock_guardw przeciwieństwie do kodu, który używa unique_lock. Jedynym sposobem na osiągnięcie tego, o co prosisz, jest celowe przerwanie enkapsulacji lock_guardklasy i wystawienie jej implementacji na inną klasę (w tym przypadku na condition_variable). Jest to trudna cena za wątpliwą korzyść użytkownika zmiennej warunkowej, która nie musi pamiętać różnicy między tymi dwoma typami zamków.
ComicSansMS,
4
@Chris Skąd pomysł, który condition_variable_any.waitbędzie działał z lock_guard? Norma wymaga dostarczonego typu zamka, aby spełnić BasicLockablewymaganie (§30.5.2), co lock_guardnie spełnia. Tylko bazowy muteks ma, ale z powodów, o których wspomniałem wcześniej, interfejs lock_guardnie zapewnia dostępu do muteksu.
ComicSansMS,
11

Są pewne rzeczy wspólne między lock_guarda unique_locki pewnymi różnicami.

Ale w kontekście zadanego pytania kompilator nie zezwala na użycie lock_guardkombinacji w połączeniu ze zmienną warunkową, ponieważ gdy wywołanie wątku czeka na zmienną warunkową, muteks zostaje odblokowany automatycznie, a inne powiadomienia / wątek powiadamiają o tym bieżący wątek jest wywoływany (wychodzi z oczekiwania), zamek jest ponownie nabywany.

Zjawisko to jest sprzeczne z zasadą lock_guard. lock_guardmożna zbudować tylko raz, a zniszczyć tylko raz.

Dlatego lock_guardnie można go używać w połączeniu ze zmienną warunkową, ale unique_lockmożna to zrobić (ponieważ unique_lockmożna go kilkakrotnie zablokować i odblokować).

Sandeep
źródło
5
he compiler does not allow using a lock_guard in combination with a condition variableTo nieprawda. To z pewnością nie pozwoli i pracy doskonale z lock_guardna notify()ing stronie. Tylko wait()strona int wymaga a unique_lock, ponieważ wait()musi zwolnić blokadę podczas sprawdzania warunku.
underscore_d
0

Nie są tak naprawdę takimi samymi muteksami, lock_guard<muType>mają prawie takie same jak std::mutex, z tą różnicą, że ich żywotność kończy się na końcu zakresu (nazywany D-tor), więc jasna definicja tych dwóch muteksów:

lock_guard<muType> ma mechanizm posiadania muteksu na czas trwania bloku o zasięgu.

I

unique_lock<muType> jest opakowaniem umożliwiającym odroczenie blokady, ograniczone czasowo próby blokowania, blokowanie rekurencyjne, przeniesienie własności blokady i użycie ze zmiennymi warunkowymi.

Oto przykładowa implementacja:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <chrono>

using namespace std::chrono;

class Product{

   public:

       Product(int data):mdata(data){
       }

       virtual~Product(){
       }

       bool isReady(){
       return flag;
       }

       void showData(){

        std::cout<<mdata<<std::endl;
       }

       void read(){

         std::this_thread::sleep_for(milliseconds(2000));

         std::lock_guard<std::mutex> guard(mmutex);

         flag = true;

         std::cout<<"Data is ready"<<std::endl;

         cvar.notify_one();

       }

       void task(){

       std::unique_lock<std::mutex> lock(mmutex);

       cvar.wait(lock, [&, this]() mutable throw() -> bool{ return this->isReady(); });

       mdata+=1;

       }

   protected:

    std::condition_variable cvar;
    std::mutex mmutex;
    int mdata;
    bool flag = false;

};

int main(){

     int a = 0;
     Product product(a);

     std::thread reading(product.read, &product);
     std::thread setting(product.task, &product);

     reading.join();
     setting.join();


     product.showData();
    return 0;
}

W tym przykładzie użyłem unique_lock<muType>zcondition variable

rekkalmd
źródło
-5

Jak wspomnieli inni, std :: unique_lock śledzi status blokady muteksu, dzięki czemu można odroczyć blokowanie do momentu zbudowania blokady i odblokować przed zniszczeniem blokady. std :: lock_guard nie pozwala na to.

Wydaje się, że nie ma powodu, dla którego funkcje oczekiwania std :: condition_variable nie powinny brać blokady lock_guard, jak również unikalnego_locka, ponieważ za każdym razem, gdy kończy się oczekiwanie (z dowolnego powodu) muteks jest automatycznie odzyskiwany, aby nie spowodował żadnego naruszenia semantycznego. Jednak zgodnie ze standardem, aby użyć std :: lock_guard ze zmienną warunku, musisz użyć std :: condition_variable_any zamiast std :: condition_variable.

Edycja : usunięto „Korzystanie z interfejsu pthreads std :: condition_variable i std :: condition_variable_any powinny być identyczne”. Przyglądając się implementacji gcc:

  • std :: condition_variable :: wait (std :: unique_lock &) po prostu wywołuje pthread_cond_wait () na podstawowej zmiennej warunku pthread w odniesieniu do muteksu przechowywanego przez unique_lock (a więc może równie dobrze zrobić to samo dla lock_guard, ale nie robi tego, ponieważ standard nie przewiduje tego)
  • std :: condition_variable_any może współpracować z dowolnym blokowanym obiektem, w tym takim, który wcale nie jest blokadą mutex (dlatego mógłby nawet pracować z semaforem międzyprocesowym)
Chris Vine
źródło