Preciso adquirir o bloqueio antes de chamar condition_variable.notify_one ()?

90

Estou um pouco confuso sobre o uso de std::condition_variable. Eu entendo que tenho que criar um unique_lockem um mutexantes de ligar condition_variable.wait(). O que não consigo descobrir é se também devo adquirir um bloqueio exclusivo antes de ligar notify_one()ou notify_all().

Os exemplos em cppreference.com são conflitantes. Por exemplo, a página notification_one dá este exemplo:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

Aqui, o bloqueio não é adquirido para o primeiro notify_one(), mas é adquirido para o segundo notify_one(). Olhando outras páginas com exemplos, vejo coisas diferentes, principalmente não obtendo o bloqueio.

  • Posso escolher bloquear o mutex antes de ligar notify_one()e por que escolheria bloqueá-lo?
  • No exemplo dado, por que não há bloqueio para a primeira notify_one(), mas para chamadas subsequentes. Este exemplo está errado ou existe algum fundamento lógico?
Peter Smit
fonte

Respostas:

77

Você não precisa estar segurando um bloqueio ao chamar condition_variable::notify_one(), mas não é errado no sentido de que ainda é um comportamento bem definido e não um erro.

No entanto, pode ser uma "pessimização", uma vez que qualquer thread em espera tornado executável (se houver) tentará imediatamente adquirir o bloqueio que o thread de notificação contém. Acho que é uma boa regra evitar segurar o bloqueio associado a uma variável de condição ao chamar notify_one()ou notify_all(). Veja Pthread Mutex: pthread_mutex_unlock () consome muito tempo para um exemplo em que liberar um bloqueio antes de chamar o pthread equivalente de notify_one()desempenho mensurável.

Lembre-se de que a lock()chamada no whileloop é necessária em algum ponto, porque o bloqueio precisa ser retido durante a while (!done)verificação da condição do loop. Mas não precisa ser colocado em espera para a chamada para notify_one().


2016-02-27 : Grande atualização para responder a algumas questões nos comentários sobre se há uma condição de corrida se o bloqueio não ajuda na notify_one()chamada. Sei que esta atualização está atrasada porque a pergunta foi feita há quase dois anos, mas gostaria de responder à pergunta de @Biscoito sobre uma possível condição de corrida se o produtor ( signals()neste exemplo) ligar notify_one()antes do consumidor ( waits()neste exemplo) for capaz de ligar wait().

A chave é o que acontece i- esse é o objeto que realmente indica se o consumidor tem ou não "trabalho" a fazer. O condition_variableé apenas um mecanismo para permitir que o consumidor espere com eficiência por uma mudança para i.

O produtor precisa segurar o bloqueio durante a atualização i, e o consumidor deve segurar o bloqueio enquanto verifica ie chama condition_variable::wait()(se precisar esperar). Nesse caso, a chave é que deve ser a mesma instância de segurar a fechadura (geralmente chamada de seção crítica) quando o consumidor faz essa verificação e espera. Uma vez que a seção crítica é realizada quando o produtor atualiza ie quando o consumidor verifica e espera i, não há oportunidade de imudar entre quando o consumidor verifica ie quando ele liga condition_variable::wait(). Este é o ponto crucial para um uso adequado das variáveis ​​de condição.

O padrão C ++ diz que condition_variable :: wait () se comporta como o seguinte quando chamado com um predicado (como neste caso):

while (!pred())
    wait(lock);

Existem duas situações que podem ocorrer quando o consumidor verifica i:

  • se ifor 0, então o consumidor chama cv.wait(), então iainda será 0 quando a wait(lock)parte da implementação for chamada - o uso adequado dos bloqueios garante isso. Neste caso, o produtor não tem oportunidade de chamar o condition_variable::notify_one()em seu whileloop até que o consumidor tenha chamado cv.wait(lk, []{return i == 1;})(e a wait()chamada tenha feito tudo o que precisa para 'pegar' uma notificação - wait()não irá liberar o bloqueio até que tenha feito isso ) Portanto, neste caso, o consumidor não pode perder a notificação.

  • se ijá for 1 quando o consumidor ligar cv.wait(), a wait(lock)parte da implementação nunca será chamada porque o while (!pred())teste fará com que o loop interno termine. Nesta situação, não importa quando ocorre a chamada para Notice_one () - o consumidor não irá bloquear.

O exemplo aqui tem a complexidade adicional de usar a donevariável para sinalizar de volta ao encadeamento do produtor que o consumidor reconheceu isso i == 1, mas eu não acho que isso mude a análise porque todo o acesso a done(para leitura e modificação ) são feitos nas mesmas seções críticas que envolvem ie o condition_variable.

Se você olhar para a questão que @ EH9 apontou, sincronização não é confiável usando std :: atômica e std :: condition_variable , você vai ver uma condição de corrida. No entanto, o código postado nessa questão viola uma das regras fundamentais de uso de uma variável de condição: ele não contém uma única seção crítica ao executar uma verificação e espera.

Nesse exemplo, o código se parece com:

if (--f->counter == 0)      // (1)
    // we have zeroed this fence's counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

Você notará que wait()em # 3 é executado enquanto segura f->resume_mutex. Mas a verificação para saber se o wait()é ou não necessário na etapa # 1 não é feita enquanto segura o bloqueio (muito menos continuamente para verificar e esperar), que é um requisito para o uso adequado das variáveis ​​de condição). Acredito que a pessoa que tem o problema com aquele trecho de código pensou que já que f->counterera um std::atomictipo isso atenderia ao requisito. No entanto, a atomicidade fornecida por std::atomicnão se estende à chamada subsequente de f->resume.wait(lock). Neste exemplo, há uma corrida entre quando f->counteré marcado (etapa 1) e quando o wait()é chamado (etapa 3).

Essa raça não existe no exemplo desta questão.

Michael Burr
fonte
2
tem implicações mais profundas: domaigne.com/blog/computing/… Notavelmente, o problema de pthread que você mencionou deve ser resolvido por uma versão mais recente ou uma versão construída com os sinalizadores corretos. (para habilitar a wait morphingotimização) Regra prática explicada neste link: notificar com bloqueio é melhor em situações com mais de 2 threads para resultados mais previsíveis.
v.oddou
6
@Michael: Para meu entendimento, o consumidor precisa eventualmente ligar the_condition_variable.wait(lock);. Se não houver necessidade de bloqueio para sincronizar o produtor e o consumidor (digamos que o subjacente seja uma fila spsc sem bloqueio), esse bloqueio não terá nenhum propósito se o produtor não o bloquear. Por mim tudo bem. Mas não há risco para uma raça rara? Se o produtor não mantiver o bloqueio, ele não poderia chamar Notice_one enquanto o consumidor está logo antes da espera? Então o consumidor entra na espera e não vai acordar ...
Cookie
1
por exemplo, digamos que no código acima o consumidor esteja std::cout << "Waiting... \n";enquanto o produtor o faz cv.notify_one();, a chamada de despertar desaparece ... Ou estou faltando alguma coisa aqui?
Cookie
1
@Bolacha. Sim, há uma condição de corrida aí. Consulte stackoverflow.com/questions/20982270/…
eh9
1
@ eh9: Droga, acabei de descobrir a causa de um bug que travava meu código de vez em quando, graças ao seu comentário. Foi devido a este caso exato de condição de corrida. Desbloquear o mutex após a notificação resolveu totalmente o problema ... Muito obrigado!
Galinette
10

Situação

Usando vc10 e Boost 1.56, implementei uma fila simultânea muito parecida com a que esta postagem do blog sugere. O autor desbloqueia o mutex para minimizar a contenção, ou seja, notify_one()é chamado com o mutex desbloqueado:

void push(const T& item)
{
  std::unique_lock<std::mutex> mlock(mutex_);
  queue_.push(item);
  mlock.unlock();     // unlock before notificiation to minimize mutex contention
  cond_.notify_one(); // notify one waiting thread
}

O desbloqueio do mutex é apoiado por um exemplo na documentação do Boost :

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

Problema

Ainda assim, isso levou ao seguinte comportamento errático:

  • enquanto notify_one()se não foi chamado ainda cond_.wait()ainda pode ser interrompida atravésboost::thread::interrupt()
  • uma vez notify_one()foi chamado de cond_.wait()deadlocks pela primeira vez ; a espera não pode terminar por boost::thread::interrupt()ou boost::condition_variable::notify_*()mais.

Solução

A remoção da linha mlock.unlock()fez o código funcionar conforme o esperado (notificações e interrupções encerram a espera). Observe que notify_one()é chamado com o mutex ainda bloqueado, ele é desbloqueado logo após ao sair do escopo:

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

Isso significa que, pelo menos com minha implementação de thread particular, o mutex não deve ser desbloqueado antes de chamar boost::condition_variable::notify_one(), embora ambas as maneiras pareçam corretas.

Matthäus Brandl
fonte
Você relatou esse problema ao Boost.Thread? Não consigo encontrar uma tarefa semelhante aqui svn.boost.org/trac/boost/…
magras
@magras Infelizmente não, não faço ideia porque não considerei isso. E infelizmente não consigo reproduzir este erro usando a fila mencionada.
Matthäus Brandl
Não tenho certeza se um despertar precoce pode causar um impasse. Especificamente, se você sair de cond_.wait () em pop () após push () liberar o mutex da fila, mas antes de notificar_one () ser chamado - Pop () deve ver a fila não vazia e consumir a nova entrada em vez de esperando. se você sair de cond_.wait () enquanto o push () estiver atualizando a fila, o bloqueio deve ser mantido por push (), portanto, pop () deve bloquear esperando que o bloqueio seja liberado. Qualquer outro despertar antecipado manteria o bloqueio, impedindo push () de modificar a fila antes que pop () chame o próximo wait (). O que eu perdi?
Kevin de
5

Como outros apontaram, você não precisa estar segurando o bloqueio ao chamar notify_one(), em termos de condições de corrida e problemas relacionados ao encadeamento. No entanto, em alguns casos, pode ser necessário segurar o bloqueio para evitar que o condition_variableseja destruído antes de notify_one()ser chamado. Considere o seguinte exemplo:

thread t;

void foo() {
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    t = std::thread([&]() {
        {
            std::lock_guard<std::mutex> l(m);  // (1)
            done = true;  // (2)
        }  // (3)
        cv.notify_one();  // (4)
    });  // (5)

    std::unique_lock<std::mutex> lock(m);  // (6)
    cv.wait(lock, [&done]() { return done; });  // (7)
}

void main() {
    foo();  // (8)
    t.join();  // (9)
}

Suponha que haja uma mudança de contexto para a thread recém-criada tdepois de criá-la, mas antes de começarmos a esperar pela variável de condição (em algum lugar entre (5) e (6)). O thread tadquire o bloqueio (1), define a variável de predicado (2) e, em seguida, libera o bloqueio (3). Suponha que haja outra troca de contexto neste ponto antes de notify_one()(4) ser executado. A thread principal adquire o bloqueio (6) e executa a linha (7), ponto no qual o predicado retorna truee não há razão para esperar, então ele libera o bloqueio e continua. fooretorna (8) e as variáveis ​​em seu escopo (incluindo cv) são destruídas. Antes que o thread tpudesse se juntar ao thread principal (9), ele deve terminar sua execução, então continua de onde parou para executarcv.notify_one()(4), ponto em que cvjá está destruído!

A correção possível neste caso é manter o bloqueio ao chamar notify_one(ou seja, remover o escopo que termina na linha (3)). Fazendo isso, garantimos que as tchamadas de thread notify_oneantes cv.waitpossam verificar a variável de predicado recém-configurada e continuar, uma vez que seria necessário adquirir o bloqueio, que t está atualmente em espera, para fazer a verificação. Portanto, garantimos que cvnão seja acessado por thread tapós os fooretornos.

Para resumir, o problema neste caso específico não é realmente sobre encadeamento, mas sobre os tempos de vida das variáveis ​​capturadas por referência. cvé capturado por referência por meio do encadeamento t, portanto, você deve garantir que cvpermaneça ativo durante a execução do encadeamento. Os outros exemplos aqui apresentados não apresentam esse problema, pois os objetos condition_variablee mutexsão definidos no escopo global, portanto, são garantidos que permanecerão ativos até o encerramento do programa.

Cantunca
fonte
1

@Michael Burr está correto. condition_variable::notify_onenão requer um bloqueio na variável. Nada impede que você use uma fechadura nessa situação, como o exemplo ilustra.

No exemplo fornecido, o bloqueio é motivado pelo uso simultâneo da variável i. Como o signalsthread modifica a variável, ele precisa garantir que nenhum outro thread a acesse durante esse tempo.

Os bloqueios são usados ​​para qualquer situação que requeira sincronização , não acho que podemos afirmar isso de uma forma mais geral.

didierc
fonte
claro, mas além disso, eles também precisam ser usados ​​em combinação com variáveis ​​de condição para que todo o padrão realmente funcione. notavelmente, a waitfunção de variável de condição está liberando o bloqueio dentro da chamada e retorna somente após ter readquirido o bloqueio. após o qual você pode verificar com segurança sua condição, porque você adquiriu os "direitos de leitura", digamos. se ainda não for o que você está esperando, volte para wait. este é o padrão. btw, este exemplo NÃO respeita isso.
v.oddou
1

Em alguns casos, quando o cv pode estar ocupado (bloqueado) por outros threads. Você precisa obter o bloqueio e liberá-lo antes de notificar _ * ().
Caso contrário, a notificação _ * () pode não ser executada.

Fan Jing
fonte
1

Apenas adicionando esta resposta porque acho que a resposta aceita pode ser enganosa. Em todos os casos, você precisará bloquear o mutex, antes de chamar notificar_one () em algum lugar para que seu código seja thread-safe, embora você possa desbloqueá-lo novamente antes de realmente chamar notificar _ * ().

Para esclarecer, você DEVE fazer o bloqueio antes de entrar em wait (lk) porque wait () desbloqueia lk e seria um comportamento indefinido se o bloqueio não estivesse bloqueado. Este não é o caso de notificar_one (), mas você precisa ter certeza de não chamar notificar _ * () antes de inserir wait () e fazer com que essa chamada desbloqueie o mutex; o que, obviamente, só pode ser feito bloqueando o mesmo mutex antes de chamar notificar _ * ().

Por exemplo, considere o seguinte caso:

std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
    cv.notify_one();
}

bool start()
{
  if (count.fetch_add(1) >= 0)
    return true;
  // Failure.
  stop();
  return false;
}

void cancel()
{
  if (count.fetch_sub(1000) == 0)  // Reached -1000?
    return;
  // Wait till count reached -1000.
  std::unique_lock<std::mutex> lk(cancel_mutex);
  cancel_cv.wait(lk);
}

Aviso : este código contém um bug.

A ideia é a seguinte: threads chamam start () e stop () em pares, mas apenas enquanto start () retornar true. Por exemplo:

if (start())
{
  // Do stuff
  stop();
}

Um (outro) thread em algum ponto chamará cancel () e após retornar de cancel () destruirá os objetos que são necessários em 'Fazer coisas'. No entanto, cancel () não deve retornar enquanto houver threads entre start () e stop (), e uma vez que cancel () execute sua primeira linha, start () sempre retornará falso, então nenhuma nova thread entrará no 'Do área de coisas.

Funciona certo?

O raciocínio é o seguinte:

1) Se qualquer thread executar com sucesso a primeira linha de start () (e, portanto, retornará true), então nenhuma thread executou a primeira linha de cancel () ainda (assumimos que o número total de threads é muito menor que 1000 em caminho).

2) Além disso, enquanto uma thread executou com sucesso a primeira linha de start (), mas ainda não a primeira linha de stop (), então é impossível que qualquer thread execute com sucesso a primeira linha de cancel () (note que apenas uma thread sempre chama cancel ()): o valor retornado por fetch_sub (1000) será maior que 0.

3) Uma vez que um thread tenha executado a primeira linha de cancel (), a primeira linha de start () sempre retornará falso e um thread chamando start () não entrará mais na área 'Do stuff'.

4) O número de chamadas para iniciar () e parar () são sempre balanceadas, então após a primeira linha de cancel () ser executada sem sucesso, sempre haverá um momento em que uma (última) chamada para parar () causa contagem para atingir -1000 e, portanto, notificar_one () para ser chamado. Observe que isso só pode acontecer quando a primeira linha de cancelamento resultou na falha do thread.

Além de um problema de fome onde tantos threads estão chamando start () / stop () que a contagem nunca atinge -1000 e cancel () nunca retorna, o que se pode aceitar como "improvável e nunca durando muito", há outro bug:

É possível que haja um thread dentro da área 'Fazer coisas', digamos que ele esteja apenas chamando stop (); naquele momento, uma thread executa a primeira linha de cancel () lendo o valor 1 com fetch_sub (1000) e caindo. Mas antes de pegar o mutex e / ou fazer a chamada para esperar (lk), o primeiro thread executa a primeira linha de stop (), lê -999 e chama cv.notify_one ()!

Então esta chamada para notificar_one () é feita ANTES de estarmos esperando () - na variável de condição! E o programa travaria indefinidamente.

Por esta razão, não devemos ser capazes de chamar notificar_one () até que chamemos wait (). Observe que o poder de uma variável de condição reside no fato de que ela é capaz de desbloquear atomicamente o mutex, verificar se uma chamada para notificar_one () aconteceu e ir dormir ou não. Você não pode enganar, mas você fazer necessidade de manter o mutex bloqueado sempre que você fazer alterações em variáveis que podem mudar a condição de falso para verdadeiro e mantê -la trancada ao chamar notify_one () por causa de condições de corrida como descrito aqui.

Neste exemplo, entretanto, não há condição. Por que não usei como condição 'count == -1000'? Porque isso não é nada interessante aqui: assim que -1000 for atingido, temos certeza de que nenhum novo tópico entrará na área 'Fazer coisas'. Além disso, as threads ainda podem chamar start () e irão incrementar a contagem (para -999 e -998 etc), mas não nos importamos com isso. A única coisa que importa é que -1000 foi alcançado - para que possamos saber com certeza que não há mais tópicos na área 'Fazer coisas'. Temos certeza de que este é o caso quando notificar_one () está sendo chamado, mas como ter certeza de não chamar notificar_one () antes de cancelar () bloquear seu mutex? Apenas bloquear cancel_mutex antes de notificar_one () não vai ajudar, é claro.

O problema é que, apesar de não estarmos esperando por uma condição, ainda existe uma condição e precisamos bloquear o mutex

1) antes que essa condição seja alcançada 2) antes de chamar notifiquem_um.

O código correto, portanto, torna-se:

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
  {
    cancel_mutex.lock();
    cancel_mutex.unlock();
    cv.notify_one();
  }
}

[... mesmo começo () ...]

void cancel()
{
  std::unique_lock<std::mutex> lk(cancel_mutex);
  if (count.fetch_sub(1000) == 0)
    return;
  cancel_cv.wait(lk);
}

Claro que este é apenas um exemplo, mas outros casos são muito semelhantes; em quase todos os casos em que você usa uma variável condicional, você precisará ter aquele mutex bloqueado (em breve) antes de chamar not_one (), ou então é possível chamá-lo antes de chamar wait ().

Observe que eu desbloqueei o mutex antes de chamar o notificar_one () neste caso, porque caso contrário, há a (pequena) chance de que a chamada para o notificador_one () acorde o segmento esperando pela variável de condição que tentará pegar o mutex e bloco, antes de liberarmos o mutex novamente. Isso é apenas um pouco mais lento do que o necessário.

Este exemplo foi meio especial porque a linha que muda a condição é executada pela mesma thread que chama wait ().

Mais comum é o caso em que uma thread simplesmente espera que uma condição se torne verdadeira e outra thread obtém o bloqueio antes de alterar as variáveis ​​envolvidas naquela condição (fazendo com que possivelmente se torne verdadeira). Nesse caso, o mutex é bloqueado imediatamente antes (e depois) da condição se tornar verdadeira - portanto, está totalmente ok apenas desbloquear o mutex antes de chamar o notificador _ * () nesse caso.

Carlo madeira
fonte