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?

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.


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.


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.


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).


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


-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)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.