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

348

Ich habe zwei Anwendungsfälle.

A. Ich möchte den Zugriff von zwei Threads auf eine Warteschlange synchronisieren.

B. Ich möchte den Zugriff von zwei Threads auf eine Warteschlange synchronisieren und eine Bedingungsvariable verwenden, da einer der Threads darauf wartet, dass der Inhalt vom anderen Thread in der Warteschlange gespeichert wird.

Für Anwendungsfall AI siehe Codebeispiel using std::lock_guard<>. Für Anwendungsfall-BI siehe Codebeispiel using std::unique_lock<>.

Was ist der Unterschied zwischen den beiden und welchen sollte ich in welchem ​​Anwendungsfall verwenden?

chmike
quelle

Antworten:

344

Der Unterschied besteht darin, dass Sie a sperren und entsperren können std::unique_lock. std::lock_guardwird beim Bau nur einmal gesperrt und bei Zerstörung entsperrt.

Für Anwendungsfall B benötigen Sie also definitiv eine std::unique_lockfür die Bedingungsvariable. In Fall A hängt es davon ab, ob Sie den Schutz wieder verriegeln müssen.

std::unique_lockhat andere Funktionen, die es ermöglichen, zB: ohne sofortiges Sperren des Mutex zu konstruieren, aber den RAII-Wrapper zu erstellen (siehe hier ).

std::lock_guardbietet auch einen praktischen RAII-Wrapper, kann jedoch mehrere Mutexe nicht sicher sperren. Es kann verwendet werden, wenn Sie einen Wrapper für einen begrenzten Bereich benötigen, z. B.: Eine Mitgliedsfunktion:

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

Um eine Frage von chmike zu klären, sind standardmäßig std::lock_guardund std::unique_lockgleich. So im obigen Fall, könnten Sie ersetzen std::lock_guardmit std::unique_lock. Allerdings std::unique_lockhaben könnte mehr Aufwand ein bisschen.

Beachten Sie, dass in diesen Tagen sollte man verwenden std::scoped_lockstatt std::lock_guard.

Stephan Dollberg
quelle
2
Mit der Anweisung std :: unique_lock <std :: mutex> lock (myMutex); Wird der Mutex vom Konstruktor gesperrt?
Chmike
3
@chmike Ja, das wird es. Einige Klarstellungen hinzugefügt.
Stephan Dollberg
10
@chmike Nun, ich denke, es geht weniger um Effizienz als um Funktionalität. Wenn std::lock_guardes für Ihren Fall A ausreicht, sollten Sie es verwenden. Dies vermeidet nicht nur unnötigen Overhead, sondern zeigt dem Leser auch die Absicht, diesen Schutz niemals freizuschalten.
Stephan Dollberg
5
@chmike: Theoretisch ja. Mutices sind jedoch nicht gerade leichte Konstrukte, so dass der zusätzliche Overhead des unique_lockwahrscheinlich durch die Kosten für das tatsächliche Sperren und Entsperren des Mutex in den Schatten gestellt wird (wenn der Compiler diesen Overhead nicht optimiert hätte, was möglich sein könnte).
Grizzly
6
So for usecase B you definitely need a std::unique_lock for the condition variable- ja, aber nur in dem Thread, der cv.wait()s ist, weil diese Methode den Mutex atomar freigibt. In dem anderen Thread, in dem Sie die gemeinsam genutzten Variablen aktualisieren und dann aufrufen cv.notify_one(), reicht ein einfaches lock_guardaus, um den Mutex im Gültigkeitsbereich zu sperren ... es sei denn, Sie tun etwas Aufwändigeres, das ich mir nicht vorstellen kann! zB en.cppreference.com/w/cpp/thread/condition_variable - funktioniert für mich :)
underscore_d
114

lock_guardund unique_locksind so ziemlich das Gleiche; lock_guardist eine eingeschränkte Version mit einer eingeschränkten Schnittstelle.

A lock_guardhält immer ein Schloss von seiner Konstruktion bis zu seiner Zerstörung. A unique_lockkann ohne sofortige Sperrung erstellt werden, kann zu jedem Zeitpunkt seiner Existenz entsperrt werden und kann das Eigentum an der Sperre von einer Instanz auf eine andere übertragen.

Sie verwenden also immer lock_guard, es sei denn, Sie benötigen die Funktionen von unique_lock. A condition_variablebraucht a unique_lock.

Sebastian Redl
quelle
11
A condition_variable needs a unique_lock.- ja aber nur auf der wait()ing Seite, wie in meinem Kommentar zu inf ausgeführt.
underscore_d
48

Verwenden lock_guardSie diese Option, es sei denn, Sie müssen in der Lage sein, unlockden Mutex dazwischen manuell zu erstellen, ohne den zu zerstören lock.

condition_variableEntsperrt insbesondere seinen Mutex, wenn er bei Anrufen in den Ruhezustand geht wait. Deshalb lock_guardreicht a hier nicht aus.

ComicSansMS
quelle
Das Übergeben eines lock_guard an eine der Wartemethoden der bedingten Variablen wäre in Ordnung, da der Mutex immer wieder abgerufen wird, wenn das Warten endet, aus welchem ​​Grund auch immer. Der Standard bietet jedoch nur eine Schnittstelle für unique_lock. Dies könnte als Standardmangel angesehen werden.
Chris Vine
3
@Chris In diesem Fall würden Sie die Kapselung immer noch unterbrechen. Die Wartemethode müsste in der Lage sein, den Mutex aus dem zu extrahieren lock_guardund ihn zu entsperren, wodurch die Klasseninvariante der Wache vorübergehend unterbrochen wird. Auch wenn dies für den Benutzer unsichtbar ist, würde ich dies als legitimen Grund dafür betrachten, die Verwendung lock_guardin diesem Fall nicht zuzulassen .
ComicSansMS
Wenn ja, wäre es unsichtbar und nicht nachweisbar. gcc-4.8 macht es. wait (unique_lock <mutex> &) ruft __gthread_cond_wait (& _ M_cond, __lock.mutex () -> native_handle ()) auf (siehe libstdc ++ - v3 / src / c ++ 11 / condition_variable.cc) und ruft pthread_cond_wait () auf (siehe libgcc /gthr-posix.h). Das gleiche könnte für lock_guard gemacht werden (aber nicht, weil es nicht im Standard für condition_variable enthalten ist).
Chris Vine
4
@Chris Der Punkt ist, lock_guarddass das Abrufen des zugrunde liegenden Mutex überhaupt nicht möglich ist. Dies ist eine bewusste Einschränkung, um eine einfachere Argumentation für Code zu ermöglichen, der verwendet wird, lock_guardim Gegensatz zu Code, der a verwendet unique_lock. Die einzige Möglichkeit, das zu erreichen, was Sie verlangen, besteht darin, die Kapselung der lock_guardKlasse absichtlich zu unterbrechen und ihre Implementierung einer anderen Klasse (in diesem Fall der condition_variable) auszusetzen . Dies ist ein schwieriger Preis für den fragwürdigen Vorteil des Benutzers einer Bedingungsvariablen, der sich nicht an den Unterschied zwischen den beiden Sperrtypen erinnern muss.
ComicSansMS
4
@ Chris Woher hast du die Idee, dass condition_variable_any.waitmit einem funktionieren würde lock_guard? Die Norm verlangt, dass der bereitgestellte BasicLockableSperrtyp die Anforderung erfüllt (§30.5.2), was lock_guardnicht der Fall ist. Nur der zugrunde liegende Mutex funktioniert, aber aus Gründen, auf die ich bereits hingewiesen habe, bietet die Schnittstelle von lock_guardkeinen Zugriff auf den Mutex.
ComicSansMS
11

Es gibt bestimmte Gemeinsamkeiten zwischen lock_guardund unique_lockund bestimmte Unterschiede.

Im Kontext der gestellten Frage erlaubt der Compiler jedoch nicht die Verwendung einer lock_guardin Kombination mit einer Bedingungsvariablen, da der Mutex automatisch entsperrt wird, wenn ein Thread auf eine Bedingungsvariable wartet, und wenn andere Threads / Threads und der aktuelle Thread benachrichtigt werden wird aufgerufen (kommt aus der Wartezeit), wird die Sperre wiedererlangt.

Dieses Phänomen widerspricht dem Prinzip von lock_guard. lock_guardkann nur einmal konstruiert und nur einmal zerstört werden.

Daher lock_guardkann nicht in Kombination mit einer Bedingungsvariablen verwendet werden, aber a unique_lockkann (weil unique_lockmehrmals gesperrt und entsperrt werden kann).

Sandeep
quelle
5
he compiler does not allow using a lock_guard in combination with a condition variableDas ist falsch. Es sicherlich nicht erlauben und arbeitet perfekt mit einem lock_guardauf der notify()ing Seite. Nur die wait()int-Seite erfordert ein unique_lock, da wait()die Sperre freigegeben werden muss, während auf den Zustand geprüft wird.
underscore_d
0

Sie sind nicht wirklich die gleichen Mutexe, lock_guard<muType>haben fast die gleichen wie std::mutex, mit dem Unterschied, dass ihre Lebensdauer am Ende des Bereichs endet (D-tor genannt), also eine klare Definition dieser beiden Mutexe:

lock_guard<muType> hat einen Mechanismus zum Besitzen eines Mutex für die Dauer eines Bereichsblocks.

Und

unique_lock<muType> ist ein Wrapper, der verzögertes Sperren, zeitlich begrenzte Sperrversuche, rekursives Sperren, Übertragung des Sperrenbesitzes und Verwendung mit Bedingungsvariablen ermöglicht.

Hier ist ein Beispiel für die Implementierung:

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

In diesem Beispiel habe ich das unique_lock<muType>mit verwendetcondition variable

rekkalmd
quelle
-5

Wie bereits von anderen erwähnt, verfolgt std :: unique_lock den Sperrstatus des Mutex, sodass Sie das Sperren bis nach dem Aufbau des Schlosses verschieben und vor dem Zerstören des Schlosses entsperren können. std :: lock_guard erlaubt dies nicht.

Es scheint keinen Grund zu geben, warum die Wartefunktionen std :: condition_variable nicht sowohl einen lock_guard als auch einen unique_lock verwenden sollten, da der Mutex bei jedem Ende einer Wartezeit (aus welchem ​​Grund auch immer) automatisch neu erfasst wird, sodass keine semantische Verletzung auftritt. Um jedoch einen std :: lock_guard mit einer Bedingungsvariablen zu verwenden, müssen Sie gemäß dem Standard eine std :: condition_variable_any anstelle von std :: condition_variable verwenden.

Bearbeiten : gelöscht "Mit der pthreads-Schnittstelle sollten std :: condition_variable und std :: condition_variable_any identisch sein". Zum Blick auf die Implementierung von gcc:

  • std :: condition_variable :: wait (std :: unique_lock &) ruft nur pthread_cond_wait () für die zugrunde liegende pthread-Bedingungsvariable in Bezug auf den von unique_lock gehaltenen Mutex auf (und könnte dies auch für lock_guard tun, aber nicht, weil der Standard dies tut sieht das nicht vor)
  • std :: condition_variable_any kann mit jedem abschließbaren Objekt arbeiten, einschließlich eines Objekts, das überhaupt keine Mutex-Sperre ist (es könnte daher sogar mit einem Interprozess-Semaphor funktionieren).
Chris Vine
quelle