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

348

Eu tenho dois casos de uso.

A. Eu quero sincronizar o acesso por dois threads em uma fila.

B. Quero sincronizar o acesso de dois threads a uma fila e usar uma variável de condição porque um dos threads aguardará o conteúdo ser armazenado na fila pelo outro thread.

Para o caso de uso AI, veja o exemplo de código usando std::lock_guard<>. Para o caso de uso de BI, veja o exemplo de código using std::unique_lock<>.

Qual é a diferença entre os dois e qual deles devo usar em qual caso de uso?

chmike
fonte

Respostas:

344

A diferença é que você pode bloquear e desbloquear a std::unique_lock. std::lock_guardserá bloqueado apenas uma vez na construção e desbloqueado na destruição.

Portanto, para o caso de uso B, você definitivamente precisa de um std::unique_lockpara a variável de condição. No caso A, depende se você precisa trancar a proteção novamente.

std::unique_lockpossui outros recursos que permitem, por exemplo: ser construído sem bloquear o mutex imediatamente, mas criar o wrapper RAII (veja aqui ).

std::lock_guardtambém fornece um invólucro RAII conveniente, mas não pode bloquear vários mutexes com segurança. Pode ser usado quando você precisar de um wrapper para um escopo limitado, por exemplo: uma função de membro:

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 esclarecer uma pergunta por chmike, por padrão std::lock_guarde std::unique_locksão os mesmos. Portanto, no caso acima, você pode substituir std::lock_guardpor std::unique_lock. No entanto, std::unique_lockpode ter um pouco mais de sobrecarga.

Note que hoje em dia deve-se usar em std::scoped_lockvez de std::lock_guard.

Stephan Dollberg
fonte
2
Com a instrução std :: unique_lock <std :: mutex> lock (myMutex); o mutex será bloqueado pelo construtor?
chmike
3
@chmike Sim, será. Adicionados alguns esclarecimentos.
Stephan Dollberg
10
@chmike Bem, acho que é menos uma questão de eficiência do que de funcionalidade. Se std::lock_guardfor suficiente para o seu caso A, você deve usá-lo. Não apenas evita sobrecargas desnecessárias, mas também mostra a intenção do leitor de que você nunca desbloqueará esta proteção.
111313 Stephan Dollberg
5
@chmike: Teoricamente sim. No entanto, Mutices não são exatamente construções leves, portanto, a sobrecarga adicional do unique_lockprovavelmente será reduzida pelo custo de realmente bloquear e desbloquear o mutex (se o compilador não otimizar essa sobrecarga, o que seria possível).
Grizzly
6
So for usecase B you definitely need a std::unique_lock for the condition variable- sim, mas apenas no segmento que cv.wait()é, porque esse método libera atomicamente o mutex. No outro segmento em que você atualiza a (s) variável (s) compartilhada (s) e depois chama cv.notify_one(), lock_guardbasta um simples bloqueio do mutex no escopo ... a menos que você esteja fazendo algo mais elaborado que não consigo imaginar! por exemplo, en.cppreference.com/w/cpp/thread/condition_variable - funciona para mim :)
underscore_d
114

lock_guarde unique_locksão praticamente a mesma coisa; lock_guardé uma versão restrita com uma interface limitada.

A lock_guardsempre mantém uma trava de sua construção até sua destruição. A unique_lockpode ser criado sem o bloqueio imediato, pode ser desbloqueado a qualquer momento da existência e pode transferir a propriedade do bloqueio de uma instância para outra.

Então você sempre usa lock_guard, a menos que precise dos recursos do unique_lock. A condition_variableprecisa de um unique_lock.

Sebastian Redl
fonte
11
A condition_variable needs a unique_lock.- sim, mas apenas no wait()lado ing, conforme elaborado no meu comentário ao inf.
underscore_d
48

Use, a lock_guardmenos que você precise ser capaz de unlockalterar manualmente o mutex sem destruir o lock.

Em particular, condition_variabledesbloqueia seu mutex ao dormir após chamadas para wait. É por isso que a lock_guardnão é suficiente aqui.

ComicSansMS
fonte
Passar um lock_guard para um dos métodos de espera da variável condicional seria bom porque o mutex sempre é readquirido quando a espera termina, por qualquer motivo. No entanto, o padrão fornece apenas uma interface para unique_lock. Isso pode ser considerado uma deficiência no padrão.
Chris Vine
3
@ Chris Você ainda quebraria o encapsulamento neste caso. O método de espera precisaria ser capaz de extrair o mutex do lock_guarde desbloqueá-lo, interrompendo temporariamente a classe invariável do guarda. Mesmo que isso ocorra de forma invisível para o usuário, consideraria uma razão legítima para não permitir o uso lock_guardneste caso.
ComicSansMS
Nesse caso, seria invisível e indetectável. O gcc-4.8 faz isso. wait (unique_lock <mutex> &) chama __gthread_cond_wait (& _ M_cond, __lock.mutex () -> native_handle ()) (consulte libstdc ++ - v3 / src / c ++ 11 / condition_variable.cc), que chama pthread_cond_wait () (consulte libgcc /gthr-posix.h). O mesmo poderia ser feito para lock_guard (mas não é porque não está no padrão para condition_variable).
Chris Vine
4
@ Chris O ponto é lock_guardque não permite recuperar o mutex subjacente. Essa é uma limitação deliberada para permitir um raciocínio mais simples sobre o código que usa, lock_guardem oposição ao código que usa a unique_lock. A única maneira de conseguir o que você pede é quebrar deliberadamente o encapsulamento da lock_guardclasse e expor sua implementação a uma classe diferente (neste caso, o condition_variable). Esse é um preço difícil de pagar pela vantagem questionável do usuário de uma variável de condição, sem precisar lembrar a diferença entre os dois tipos de bloqueio.
ComicSansMS
4
@ Chris De onde você tirou a ideia de que condition_variable_any.waitfuncionaria com um lock_guard? A norma exige que o tipo de bloqueio fornecido atenda ao BasicLockablerequisito (§30.5.2), o que lock_guardnão ocorre. Somente o mutex subjacente fornece, mas, por razões que apontei anteriormente, a interface do lock_guardnão fornece acesso ao mutex.
ComicSansMS
11

Existem certas coisas comuns entre lock_guarde unique_locke certas diferenças.

Mas, no contexto da pergunta, o compilador não permite o uso de uma lock_guardcombinação com uma variável de condição, porque quando um thread chama a espera de uma variável de condição, o mutex é desbloqueado automaticamente e quando outros threads / threads notificam e o segmento atual é invocado (sai da espera), o bloqueio é adquirido novamente.

Este fenômeno é contra o princípio de lock_guard. lock_guardpode ser construído apenas uma vez e destruído apenas uma vez.

Portanto, lock_guardnão pode ser usado em combinação com uma variável de condição, mas unique_lockpode ser (porque unique_lockpode ser bloqueado e desbloqueado várias vezes).

Sandeep
fonte
5
he compiler does not allow using a lock_guard in combination with a condition variableIsto é falso. Certamente não permitir e trabalho perfeitamente com um lock_guardno notify()lado ing. Somente o wait()lado int requer a unique_lock, porque wait()deve liberar o bloqueio enquanto verifica a condição.
underscore_d
0

Eles não são realmente os mesmos mutexes, lock_guard<muType>tem quase o mesmo que std::mutex, com uma diferença de que a vida útil termina no final do escopo (chamado D-tor), portanto, uma definição clara sobre esses dois mutexes:

lock_guard<muType> possui um mecanismo para possuir um mutex pela duração de um bloco com escopo definido.

E

unique_lock<muType> é um invólucro que permite bloqueio adiado, tentativas de bloqueio com tempo limitado, bloqueio recursivo, transferência da propriedade do bloqueio e uso com variáveis ​​de condição.

Aqui está um exemplo de implementação:

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

Neste exemplo, eu usei o unique_lock<muType>comcondition variable

rekkalmd
fonte
-5

Como já foi mencionado por outros, std :: unique_lock rastreia o status de bloqueio do mutex, para que você possa adiar o bloqueio até depois da construção do bloqueio e desbloqueá-lo antes da destruição do bloqueio. std :: lock_guard não permite isso.

Parece não haver razão para as funções de espera std :: condition_variable não receberem um lock_guard, assim como um unique_lock, porque sempre que uma espera termina (por qualquer motivo), o mutex é readquirido automaticamente, de modo a não causar nenhuma violação semântica. No entanto, de acordo com o padrão, para usar um std :: lock_guard com uma variável de condição, é necessário usar um std :: condition_variable_any em vez de std :: condition_variable.

Edit : delete "Usando a interface pthreads, std :: condition_variable e std :: condition_variable_any devem ser idênticos". Ao analisar a implementação do gcc:

  • std :: condition_variable :: wait (std :: unique_lock &) chama apenas pthread_cond_wait () na variável de condição pthread subjacente em relação ao mutex mantido por unique_lock (e, portanto, poderia fazer o mesmo com lock_guard, mas não o faz porque o padrão não prevê isso)
  • std :: condition_variable_any pode trabalhar com qualquer objeto bloqueável, incluindo um que não seja um bloqueio mutex (portanto, pode até funcionar com um semáforo entre processos)
Chris Vine
fonte