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

349

J'ai deux cas d'utilisation.

A. Je veux synchroniser l'accès par deux threads à une file d'attente.

B. Je veux synchroniser l'accès de deux threads à une file d'attente et utiliser une variable de condition car l'un des threads attendra que le contenu soit stocké dans la file d'attente par l'autre thread.

Pour le cas d'utilisation AI, voir l'exemple de code utilisant std::lock_guard<>. Pour le cas d'utilisation BI, voir l'exemple de code utilisant std::unique_lock<>.

Quelle est la différence entre les deux et laquelle dois-je utiliser dans quel cas d'utilisation?

chmike
la source

Réponses:

344

La différence est que vous pouvez verrouiller et déverrouiller a std::unique_lock. std::lock_guardsera verrouillé une seule fois lors de la construction et déverrouillé lors de la destruction.

Donc, pour le cas d'utilisation B, vous avez certainement besoin d'un std::unique_lockpour la variable de condition. Dans le cas A, cela dépend si vous devez reverrouiller la garde.

std::unique_locka d'autres fonctionnalités qui lui permettent par exemple: d'être construit sans verrouiller le mutex immédiatement mais de construire le wrapper RAII (voir ici ).

std::lock_guardfournit également un wrapper RAII pratique, mais ne peut pas verrouiller plusieurs mutex en toute sécurité. Il peut être utilisé lorsque vous avez besoin d'un wrapper pour une portée limitée, par exemple: une fonction membre:

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

Pour clarifier une question par chmike, par défaut std::lock_guardet std::unique_locksont les mêmes. Donc, dans le cas ci-dessus, vous pouvez remplacer std::lock_guardpar std::unique_lock. Cependant, std::unique_lockpourrait avoir un peu plus de frais généraux.

Notez que ces jours-ci, il faut utiliser à la std::scoped_lockplace de std::lock_guard.

Stephan Dollberg
la source
2
Avec l'instruction std :: unique_lock <std :: mutex> lock (myMutex); le mutex sera-t-il verrouillé par le constructeur?
chmike
3
@chmike Oui, ce sera le cas. Ajout de quelques précisions.
Stephan Dollberg,
10
@chmike Eh bien, je pense que c'est moins une question d'efficacité que de fonctionnalité. Si cela std::lock_guardsuffit pour votre cas A, vous devez l'utiliser. Non seulement cela évite les frais généraux inutiles, mais montre également l'intention du lecteur que vous ne déverrouillerez jamais cette garde.
Stephan Dollberg
5
@chmike: Théoriquement oui. Cependant, les mutices ne sont pas exactement des constructions légères, donc le surcoût supplémentaire du unique_lockest susceptible d'être éclipsé par le coût du verrouillage et du déverrouillage du mutex (si le compilateur n'a pas optimisé ce surcoût, ce qui pourrait être possible).
Grizzly
6
So for usecase B you definitely need a std::unique_lock for the condition variable- oui mais seulement dans le thread qui cv.wait()s, car cette méthode libère atomiquement le mutex. Dans l'autre thread où vous mettez à jour les variables partagées, puis appelez cv.notify_one(), un simple lock_guardsuffit pour verrouiller le mutex dans la portée ... à moins que vous ne fassiez quelque chose de plus élaboré que je ne peux pas imaginer! par exemple en.cppreference.com/w/cpp/thread/condition_variable - fonctionne pour moi :)
underscore_d
115

lock_guardet unique_locksont à peu près la même chose; lock_guardest une version restreinte avec une interface limitée.

A lock_guarddétient toujours un verrou de sa construction à sa destruction. Un unique_lockpeut être créé sans verrouillage immédiat, peut se déverrouiller à tout moment de son existence et peut transférer la propriété du verrou d'une instance à une autre.

Donc, vous utilisez toujours lock_guard, sauf si vous avez besoin des capacités de unique_lock. A a condition_variablebesoin d'un unique_lock.

Sebastian Redl
la source
11
A condition_variable needs a unique_lock.- oui mais seulement du wait()côté ing, comme expliqué dans mon commentaire à inf.
underscore_d
48

À utiliser lock_guardsauf si vous devez être en mesure de manuellement unlockle mutex entre les deux sans détruire le lock.

En particulier, condition_variabledéverrouille son mutex lorsqu'il s'endort lors des appels à wait. C'est pourquoi un lock_guardn'est pas suffisant ici.

ComicSansMS
la source
Passer un lock_guard à l'une des méthodes d'attente de la variable conditionnelle serait bien car le mutex est toujours réacquis à la fin de l'attente, quelle qu'en soit la raison. Cependant, la norme ne fournit qu'une interface pour unique_lock. Cela pourrait être considéré comme une lacune de la norme.
Chris Vine du
3
@Chris Vous casseriez toujours l'encapsulation dans ce cas. La méthode d'attente devrait pouvoir extraire le mutex du lock_guardet le déverrouiller, brisant ainsi temporairement l'invariant de classe du garde. Même si cela se produit invisible pour l'utilisateur, je considère que c'est une raison légitime de ne pas autoriser l'utilisation de lock_guarddans ce cas.
ComicSansMS
Si c'est le cas, il serait invisible et indétectable. gcc-4.8 le fait. wait (unique_lock <mutex> &) appelle __gthread_cond_wait (& _ M_cond, __lock.mutex () -> native_handle ()) (voir libstdc ++ - v3 / src / c ++ 11 / condition_variable.cc), qui appelle pthread_cond_wait () (voir libgcc /gthr-posix.h). La même chose pourrait être faite pour lock_guard (mais ce n'est pas le cas car il n'est pas dans la norme pour condition_variable).
Chris Vine
4
@Chris Le point est lock_guardne permet pas du tout de récupérer le mutex sous-jacent. Il s'agit d'une limitation délibérée pour permettre un raisonnement plus simple sur le code qui utilise lock_guardpar opposition au code qui utilise a unique_lock. La seule façon d'atteindre ce que vous demandez est de rompre délibérément l'encapsulation de la lock_guardclasse et d'exposer son implémentation à une classe différente (dans ce cas, la condition_variable). Il s'agit d'un prix difficile à payer pour l'avantage discutable de l'utilisateur d'une variable de condition de ne pas avoir à se rappeler la différence entre les deux types de verrouillage.
ComicSansMS
4
@Chris D'où vous est venue l'idée qui condition_variable_any.waitfonctionnerait avec un lock_guard? La norme exige que le type de verrouillage fourni réponde à l' BasicLockableexigence (§30.5.2), ce qui lock_guardn'est pas le cas. Seul son mutex sous-jacent le fait, mais pour des raisons que j'ai soulignées plus tôt, l'interface de lock_guardne fournit pas d'accès au mutex.
ComicSansMS
11

Il y a certaines choses communes entre lock_guardet unique_locket certaines différences.

Mais dans le contexte de la question posée, le compilateur n'autorise pas l'utilisation d'une lock_guardcombinaison avec une variable de condition, car lorsqu'un thread appelle attendre une variable de condition, le mutex est déverrouillé automatiquement et lorsque d'autres threads / threads notifient et le thread actuel est invoqué (sort de l'attente), le verrou est racheté.

Ce phénomène est contraire au principe de lock_guard. lock_guardne peut être construit qu'une seule fois et détruit une seule fois.

Par conséquent, lock_guardne peut pas être utilisé en combinaison avec une variable de condition, mais un unique_lockpeut l'être (car il unique_lockpeut être verrouillé et déverrouillé plusieurs fois).

Sandeep
la source
5
he compiler does not allow using a lock_guard in combination with a condition variableC'est faux. Il a certainement ne permet et fonctionne parfaitement avec lock_guardle notify()côté ing. Seul le wait()côté int nécessite un unique_lock, car il wait()doit libérer le verrou tout en vérifiant l'état.
underscore_d
0

Ce ne sont pas vraiment les mêmes mutex, lock_guard<muType>a presque la même chose std::mutex, à la différence près que sa durée de vie se termine à la fin de la portée (appelé D-tor), donc une définition claire de ces deux mutex:

lock_guard<muType> possède un mécanisme permettant de posséder un mutex pendant la durée d'un bloc délimité.

Et

unique_lock<muType> est un wrapper permettant le verrouillage différé, les tentatives de verrouillage limitées dans le temps, le verrouillage récursif, le transfert de propriété du verrou et l'utilisation avec des variables de condition.

Voici un exemple d'implémentation:

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

Dans cet exemple, j'ai utilisé le unique_lock<muType>aveccondition variable

rekkalmd
la source
-5

Comme cela a été mentionné par d'autres, std :: unique_lock suit l'état verrouillé du mutex, vous pouvez donc différer le verrouillage jusqu'à la fin de la construction du verrou et le déverrouiller avant sa destruction. std :: lock_guard ne le permet pas.

Il ne semble pas y avoir de raison pour que les fonctions d'attente std :: condition_variable ne prennent pas aussi bien un lock_guard qu'un unique_lock, car chaque fois qu'une attente se termine (pour une raison quelconque), le mutex est automatiquement réacquis afin que cela ne cause aucune violation sémantique. Cependant, selon la norme, pour utiliser un std :: lock_guard avec une variable de condition, vous devez utiliser un std :: condition_variable_any au lieu de std :: condition_variable.

Edit : supprimé "L'utilisation de l'interface pthreads std :: condition_variable et std :: condition_variable_any devraient être identiques". En regardant la mise en œuvre de gcc:

  • std :: condition_variable :: wait (std :: unique_lock &) appelle juste pthread_cond_wait () sur la variable de condition pthread sous-jacente par rapport au mutex détenu par unique_lock (et pourrait donc faire de même pour lock_guard, mais pas parce que la norme ne prévoit pas cela)
  • std :: condition_variable_any peut fonctionner avec n'importe quel objet verrouillable, y compris celui qui n'est pas du tout un verrou mutex (il pourrait donc même fonctionner avec un sémaphore interprocessus)
Chris Vine
la source