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

349

Tengo dos casos de uso.

A. Quiero sincronizar el acceso de dos hilos a una cola.

B. Quiero sincronizar el acceso de dos subprocesos a una cola y usar una variable de condición porque uno de los subprocesos esperará que el otro subproceso almacene contenido en la cola.

Para el caso de uso AI vea el ejemplo de código usando std::lock_guard<>. Para el caso de uso BI, vea el ejemplo de código usando std::unique_lock<>.

¿Cuál es la diferencia entre los dos y cuál debo usar en qué caso de uso?

chmike
fuente

Respuestas:

344

La diferencia es que puedes bloquear y desbloquear a std::unique_lock. std::lock_guardserá bloqueado solo una vez en construcción y desbloqueado en destrucción.

Entonces, para el caso de uso B, definitivamente necesita un std::unique_lockpara la variable de condición. En el caso A, depende de si necesita volver a bloquear la guardia.

std::unique_locktiene otras características que le permiten, por ejemplo: construirse sin bloquear el mutex inmediatamente, pero construir el contenedor RAII (ver aquí ).

std::lock_guardtambién proporciona un conveniente contenedor RAII, pero no puede bloquear múltiples mutexes de manera segura. Se puede usar cuando necesita un contenedor para un alcance limitado, por ejemplo: una función miembro:

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

Para aclarar una pregunta por chmike, por defecto std::lock_guardy std::unique_lockson lo mismo. Entonces, en el caso anterior, podría reemplazar std::lock_guardcon std::unique_lock. Sin embargo, std::unique_lockpodría tener un poco más de gastos generales.

Tenga en cuenta que en estos días se debe usar en std::scoped_locklugar de std::lock_guard.

Stephan Dollberg
fuente
2
Con la instrucción std :: unique_lock <std :: mutex> lock (myMutex); ¿será el mutex bloqueado por el constructor?
chmike
3
@chmike Sí, lo hará. Se agregó alguna aclaración.
Stephan Dollberg
10
@chmike Bueno, creo que es menos una cuestión de eficiencia que de funcionalidad. Si std::lock_guardes suficiente para su caso A, entonces debe usarlo. No solo evita gastos innecesarios, sino que también muestra la intención al lector de que nunca desbloqueará este protector.
Stephan Dollberg
55
@chmike: Teóricamente sí. Sin embargo, los Muticios no son exactamente construcciones livianas, por lo que unique_lockes probable que la sobrecarga adicional de la misma se vea reducida por el costo de realmente bloquear y desbloquear el mutex (si el compilador no optimizó esa sobrecarga, lo que podría ser posible).
Grizzly
66
So for usecase B you definitely need a std::unique_lock for the condition variable- Sí, pero solo en el hilo que cv.wait()es, porque ese método libera atómicamente el mutex. En el otro hilo donde actualiza la (s) variable (s) compartida (s) y luego llama cv.notify_one(), un simple es lock_guardsuficiente para bloquear el mutex dentro del alcance ... ¡a menos que esté haciendo algo más elaborado que no puedo imaginar! por ejemplo, en.cppreference.com/w/cpp/thread/condition_variable - funciona para mí :)
underscore_d
115

lock_guardy unique_lockson más o menos lo mismo; lock_guardes una versión restringida con una interfaz limitada.

A lock_guardsiempre tiene un candado desde su construcción hasta su destrucción. A unique_lockse puede crear sin bloqueo inmediato, se puede desbloquear en cualquier momento de su existencia y se puede transferir la propiedad del bloqueo de una instancia a otra.

Por lo tanto, siempre usa lock_guard, a menos que necesite las capacidades de unique_lock. A condition_variablenecesita a unique_lock.

Sebastian Redl
fuente
11
A condition_variable needs a unique_lock.- Sí, pero solo por el wait()lado ing, como se explica en mi comentario a inf.
underscore_d
48

Úselo a lock_guardmenos que necesite poder unlockactivar manualmente el mutex sin destruir el lock.

En particular, condition_variabledesbloquea su exclusión mutua cuando se va a dormir a las llamadas a wait. Es por eso que a lock_guardno es suficiente aquí.

ComicSansMS
fuente
Pasar un lock_guard a uno de los métodos de espera de la variable condicional estaría bien porque el mutex siempre se vuelve a adquirir cuando finaliza la espera, por cualquier razón. Sin embargo, el estándar solo proporciona una interfaz para unique_lock. Esto podría considerarse como una deficiencia en el estándar.
Chris Vine
3
@ Chris Todavía romperías la encapsulación en este caso. El método de espera necesitaría poder extraer el mutex del lock_guardy desbloquearlo, rompiendo así temporalmente la clase invariante de la guardia. Aunque esto sucede invisible para el usuario, consideraría que es una razón legítima para no permitir el uso lock_guarden este caso.
ComicSansMS
Si es así, sería invisible e indetectable. gcc-4.8 lo hace. wait (unique_lock <mutex> &) llama a __gthread_cond_wait (& _ M_cond, __lock.mutex () -> native_handle ()) (consulte libstdc ++ - v3 / src / c ++ 11 / condition_variable.cc), que llama a pthread_cond_wait () (consulte libgcc /gthr-posix.h). Lo mismo podría hacerse para lock_guard (pero no porque no esté en el estándar para condition_variable).
Chris Vine
44
@Chris El punto es lock_guardque no permite recuperar el mutex subyacente en absoluto. Esta es una limitación deliberada para permitir un razonamiento más simple sobre el código que usa lock_guarden lugar del código que usa a unique_lock. La única forma de lograr lo que pides es rompiendo deliberadamente la encapsulación de la lock_guardclase y exponiendo su implementación a una clase diferente (en este caso, la condition_variable). Este es un precio difícil de pagar por la ventaja cuestionable del usuario de una variable de condición que no tiene que recordar la diferencia entre los dos tipos de bloqueo.
ComicSansMS
44
@ Chris ¿De dónde sacaste la idea de que condition_variable_any.waitfuncionaría con un lock_guard? El estándar requiere que el tipo de bloqueo provisto cumpla con el BasicLockablerequisito (§30.5.2), lo cual lock_guardno lo hace. Solo su mutex subyacente lo hace, pero por razones que señalé anteriormente, la interfaz de lock_guardno proporciona acceso al mutex.
ComicSansMS
11

Hay ciertas cosas comunes entre lock_guardy unique_locky ciertas diferencias.

Pero en el contexto de la pregunta formulada, el compilador no permite el uso de una lock_guardcombinación en combinación con una variable de condición, porque cuando un subproceso llama a esperar en una variable de condición, el mutex se desbloquea automáticamente y cuando otros subprocesos / subprocesos notifican y el subproceso actual se invoca (sale de la espera), el bloqueo se vuelve a adquirir.

Este fenómeno es contrario al principio de lock_guard. lock_guardpuede construirse solo una vez y destruirse solo una vez.

Por lock_guardlo tanto , no se puede usar en combinación con una variable de condición, pero sí unique_lockse puede (porque unique_lockse puede bloquear y desbloquear varias veces).

Sandeep
fuente
55
he compiler does not allow using a lock_guard in combination with a condition variableEsto es falso Ciertamente no permitir y funcionan a la perfección con una lock_guarden el notify()lado ing. Solo el wait()lado int requiere un unique_lock, porque wait()debe liberar el bloqueo mientras se verifica la condición.
underscore_d
0

En realidad no son los mismos mutexes, lock_guard<muType>tiene casi lo mismo que std::mutex, con la diferencia de que su vida útil termina al final del alcance (llamado D-tor), por lo que una definición clara sobre estos dos mutexes:

lock_guard<muType> tiene un mecanismo para poseer un mutex por la duración de un bloque de ámbito.

Y

unique_lock<muType> es un contenedor que permite el bloqueo diferido, los intentos de bloqueo de tiempo limitado, el bloqueo recursivo, la transferencia de la propiedad del bloqueo y el uso con variables de condición.

Aquí hay un ejemplo de implementación:

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

En este ejemplo, usé el unique_lock<muType>concondition variable

rekkalmd
fuente
-5

Como se ha mencionado por otros, std :: unique_lock rastrea el estado bloqueado del mutex, por lo que puede diferir el bloqueo hasta después de la construcción del bloqueo y desbloquearlo antes de la destrucción del bloqueo. std :: lock_guard no permite esto.

Parece que no hay ninguna razón por la cual las funciones de espera std :: condition_variable no deberían tomar un lock_guard y un unique_lock, porque cada vez que finaliza una espera (por cualquier razón), el mutex se vuelve a adquirir automáticamente para que no cause ninguna violación semántica. Sin embargo, según el estándar, para usar std :: lock_guard con una variable de condición, debe usar std :: condition_variable_any en lugar de std :: condition_variable.

Editar : eliminado "El uso de la interfaz pthreads std :: condition_variable y std :: condition_variable_any debería ser idéntico". Al observar la implementación de gcc:

  • std :: condition_variable :: wait (std :: unique_lock &) solo llama a pthread_cond_wait () en la variable de condición pthread subyacente con respecto a la exclusión mutua mantenida por unique_lock (y podría igualmente hacer lo mismo para lock_guard, pero no lo hace porque el estándar no prevé eso)
  • std :: condition_variable_any puede funcionar con cualquier objeto bloqueable, incluido uno que no sea un bloqueo mutex (por lo tanto, incluso podría funcionar con un semáforo entre procesos)
Chris Vine
fuente