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 while
loop é 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 i
e 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 i
e quando o consumidor verifica e espera i
, não há oportunidade de i
mudar entre quando o consumidor verifica i
e 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 i
for 0, então o consumidor chama cv.wait()
, então i
ainda 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 while
loop 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 i
já 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 done
variá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 i
e 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)
f->resume.notify_all();
else
{
unique_lock<mutex> lock(f->resume_mutex);
f->resume.wait(lock);
}
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->counter
era um std::atomic
tipo isso atenderia ao requisito. No entanto, a atomicidade fornecida por std::atomic
nã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.
wait morphing
otimização) Regra prática explicada neste link: notificar com bloqueio é melhor em situações com mais de 2 threads para resultados mais previsíveis.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 ...std::cout << "Waiting... \n";
enquanto o produtor o fazcv.notify_one();
, a chamada de despertar desaparece ... Ou estou faltando alguma coisa aqui?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:
notify_one()
se não foi chamado aindacond_.wait()
ainda pode ser interrompida atravésboost::thread::interrupt()
notify_one()
foi chamado decond_.wait()
deadlocks pela primeira vez ; a espera não pode terminar porboost::thread::interrupt()
ouboost::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 quenotify_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.fonte
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 ocondition_variable
seja destruído antes denotify_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
t
depois de criá-la, mas antes de começarmos a esperar pela variável de condição (em algum lugar entre (5) e (6)). O threadt
adquire 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 denotify_one()
(4) ser executado. A thread principal adquire o bloqueio (6) e executa a linha (7), ponto no qual o predicado retornatrue
e não há razão para esperar, então ele libera o bloqueio e continua.foo
retorna (8) e as variáveis em seu escopo (incluindocv
) são destruídas. Antes que o threadt
pudesse 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 quecv
já 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 ast
chamadas de threadnotify_one
antescv.wait
possam verificar a variável de predicado recém-configurada e continuar, uma vez que seria necessário adquirir o bloqueio, quet
está atualmente em espera, para fazer a verificação. Portanto, garantimos quecv
não seja acessado por threadt
após osfoo
retornos.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 encadeamentot
, portanto, você deve garantir quecv
permaneça ativo durante a execução do encadeamento. Os outros exemplos aqui apresentados não apresentam esse problema, pois os objetoscondition_variable
emutex
são definidos no escopo global, portanto, são garantidos que permanecerão ativos até o encerramento do programa.fonte
@Michael Burr está correto.
condition_variable::notify_one
nã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 osignals
thread 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.
fonte
wait
funçã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 parawait
. este é o padrão. btw, este exemplo NÃO respeita isso.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.
fonte
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.
fonte