É assim que as variáveis de condição são (ou foram originalmente) implementadas.
O mutex é usado para proteger a própria variável de condição . É por isso que você precisa trancado antes de esperar.
A espera irá "atomicamente" desbloquear o mutex, permitindo que outros acessem a variável de condição (para sinalização). Então, quando a variável de condição for sinalizada ou transmitida para, um ou mais dos encadeamentos na lista de espera serão acordados e o mutex será magicamente bloqueado novamente para esse encadeamento.
Você geralmente vê a seguinte operação com variáveis de condição, ilustrando como elas funcionam. O exemplo a seguir é um thread de trabalho que recebe trabalho por meio de um sinal para uma variável de condição.
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
O trabalho é realizado dentro desse loop, desde que haja algum disponível quando a espera retornar. Quando o encadeamento for sinalizado para parar de fazer o trabalho (geralmente por outro encadeamento que define a condição de saída e depois chuta a variável de condição para ativar esse encadeamento), o loop será encerrado, o mutex será desbloqueado e o encadeamento será encerrado.
O código acima é um modelo de consumidor único, pois o mutex permanece bloqueado enquanto o trabalho está sendo feito. Para uma variação de vários consumidores, você pode usar, como exemplo :
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
o que permite que outros consumidores recebam trabalho enquanto este trabalha.
A variável de condição dispensa o ônus de pesquisar alguma condição, permitindo que outro encadeamento o notifique quando algo precisa acontecer. Outro segmento pode dizer a esse segmento que o trabalho está disponível da seguinte maneira:
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
A grande maioria do que muitas vezes é chamado erroneamente de despertar espúrio geralmente era sempre porque vários encadeamentos eram sinalizados em sua pthread_cond_wait
chamada (transmissão); um retornava com o mutex, fazia o trabalho e depois aguardava.
Em seguida, o segundo thread sinalizado poderia sair quando não havia trabalho a ser feito. Portanto, você precisava ter uma variável extra indicando que o trabalho deveria ser feito (isso era inerentemente protegido por mutex com o par condvar / mutex aqui - outros threads necessários para bloquear o mutex antes de alterá-lo).
Ele era tecnicamente possível para um thread para retornar de uma espera de condição sem ser expulso por outro processo (este é um verdadeiro despertar espúria), mas, em todos os meus muitos anos trabalhando em pthreads, tanto no desenvolvimento / serviço do código e como um usuário deles, nunca recebi um deles. Talvez tenha sido apenas porque a HP teve uma implementação decente :-)
De qualquer forma, o mesmo código que tratou do caso incorreto também tratou de ativações falsas genuínas, já que o sinalizador de trabalho disponível não seria definido para eles.
do something
dentro dowhile
loop?Uma variável de condição é bastante limitada se você puder sinalizar apenas uma condição, geralmente é necessário manipular alguns dados relacionados à condição que foi sinalizada. A sinalização / ativação deve ser feita atomicamente para alcançar isso sem introduzir condições de corrida ou ser excessivamente complexo
O pthreads também pode fornecer, por razões bastante técnicas, uma ativação espúria . Isso significa que você precisa verificar um predicado, para ter certeza de que a condição foi realmente sinalizada - e distinguir isso de uma ativação espúria. A verificação dessa condição em relação à espera por ela precisa ser protegida - para que uma variável de condição precise de uma maneira de esperar / acordar atomicamente enquanto bloqueia / desbloqueia um mutex que protege essa condição.
Considere um exemplo simples em que você é notificado de que alguns dados são produzidos. Talvez outro segmento tenha criado alguns dados que você deseja e defina um ponteiro para esses dados.
Imagine um encadeamento produtor fornecendo alguns dados para outro encadeamento consumidor através de um ponteiro 'some_data'.
você naturalmente teria muitas condições de corrida, e se o outro tópico tivesse ocorrido
some_data = new_data
logo após o seu despertar, mas antes de vocêdata = some_data
Você também não pode criar seu próprio mutex para proteger esse caso .eg
Não vai funcionar, ainda há uma chance de uma condição de corrida entre acordar e pegar o mutex. Colocar o mutex antes do pthread_cond_wait não ajuda, pois agora você mantém o mutex enquanto aguarda - ou seja, o produtor nunca poderá capturar o mutex. (observe que, nesse caso, você pode criar uma segunda variável de condição para sinalizar ao produtor com quem você terminou
some_data
- embora isso se torne complexo, especialmente se você desejar muitos produtores / consumidores.)Assim, você precisa de uma maneira de liberar / capturar atomicamente o mutex ao esperar / acordar da condição. É isso que as variáveis de condição pthread fazem, e aqui está o que você faria:
(naturalmente, o produtor precisa tomar as mesmas precauções, sempre protegendo 'some_data' com o mesmo mutex e certificando-se de que não substitua some_data se some_data estiver atualmente! = NULL)
fonte
while (some_data != NULL)
ser um loop do-while para aguardar a variável de condição pelo menos uma vez?while(some_data != NULL)
serwhile(some_data == NULL)
?As variáveis de condição POSIX são sem estado. Portanto, é sua responsabilidade manter o estado. Como o estado será acessado pelos dois threads que esperam e pelos que instruem outros threads a pararem de esperar, ele deve ser protegido por um mutex. Se você acha que pode usar variáveis de condição sem um mutex, não percebeu que as variáveis de condição são sem estado.
Variáveis de condição são criadas em torno de uma condição. Encadeamentos que aguardam em uma variável de condição aguardam alguma condição. Threads que sinalizam variáveis de condição alteram essa condição. Por exemplo, um encadeamento pode estar aguardando a chegada de alguns dados. Algum outro encadeamento pode perceber que os dados chegaram. "Os dados chegaram" é a condição.
Aqui está o uso clássico de uma variável de condição, simplificada:
Veja como o encadeamento está aguardando trabalho. O trabalho é protegido por um mutex. A espera libera o mutex para que outro thread possa dar algum trabalho a esse thread. Veja como isso seria sinalizado:
Observe que você precisa do mutex para proteger a fila de trabalho. Observe que a variável de condição em si não faz ideia se há trabalho ou não. Ou seja, uma variável de condição deve estar associada a uma condição, essa condição deve ser mantida pelo seu código e, como é compartilhada entre os encadeamentos, deve ser protegida por um mutex.
fonte
Nem todas as funções de variáveis de condição requerem um mutex: apenas as operações em espera. As operações de sinal e transmissão não requerem um mutex. Uma variável de condição também não está permanentemente associada a um mutex específico; o mutex externo não protege a variável de condição. Se uma variável de condição tiver um estado interno, como uma fila de threads em espera, isso deverá ser protegido por um bloqueio interno dentro da variável de condição.
As operações de espera reúnem uma variável de condição e um mutex, porque:
Por esse motivo, a operação de espera assume como argumentos o mutex e a condição: para que ele possa gerenciar a transferência atômica de um encadeamento que possui o mutex para aguardar, para que o encadeamento não seja vítima da condição de corrida de ativação perdida .
Uma condição de corrida de ativação perdida ocorrerá se um encadeamento desistir de um mutex e, em seguida, aguardar um objeto de sincronização sem estado, mas de uma maneira que não seja atômica: existe uma janela de tempo em que o encadeamento não tem mais o bloqueio e ainda não começou a esperar no objeto. Durante essa janela, outro thread pode entrar, tornar verdadeira a condição esperada, sinalizar a sincronização sem estado e depois desaparecer. O objeto sem estado não se lembra de que foi sinalizado (é sem estado). Portanto, o encadeamento original entra em suspensão no objeto de sincronização sem estado e não é ativado, mesmo que a condição necessária já tenha se tornado verdadeira: ativação perdida.
As funções de espera da variável de condição evitam a ativação perdida, certificando-se de que o encadeamento de chamada seja registrado para capturar a ativação de maneira confiável antes de abrir o mutex. Isso seria impossível se a função de espera da variável de condição não considerasse o mutex como argumento.
fonte
pthread_cond_broadcast
e aspthread_cond_signal
operações (de que trata esta questão SO) nem tomam o mutex como argumento; apenas a condição. A especificação POSIX está aqui . O mutex é mencionado apenas em referência ao que acontece nos threads em espera quando eles são ativados.Não acho que as outras respostas sejam tão concisas e legíveis quanto esta página . Normalmente, o código em espera é mais ou menos assim:
Há três razões para agrupar o
wait()
em um mutex:signal()
antes dowait()
e nós sentiríamos falta desse despertar.check()
depende da modificação de outro encadeamento, portanto, você precisa de exclusão mútua nele.O terceiro ponto nem sempre é uma preocupação - o contexto histórico está vinculado do artigo a essa conversa .
Acordos espúrios são freqüentemente mencionados com relação a esse mecanismo (ou seja, o encadeamento em espera é ativado sem
signal()
ser chamado). No entanto, esses eventos são tratados pelo loopcheck()
.fonte
Variáveis de condição estão associadas a um mutex, porque é a única maneira de evitar a corrida que foi projetada para evitar.
Neste ponto, não há nenhum thread que sinalize a variável de condição, portanto o thread1 esperará uma eternidade, mesmo que o protectedReadyToRunVariable diga que está pronto para começar!
A única maneira de contornar isso é que as variáveis de condição liberem atomicamente o mutex enquanto simultaneamente começam a aguardar na variável de condição. É por isso que a função cond_wait requer um mutex
fonte
O mutex deve estar bloqueado quando você liga
pthread_cond_wait
; quando você o chama atomicamente, ambos desbloqueiam o mutex e depois bloqueiam a condição. Uma vez sinalizada, a condição a bloqueia atomicamente novamente e retorna.Isso permite a implementação de agendamento previsível, se desejado, em que o encadeamento que faria a sinalização pode esperar até que o mutex seja liberado para fazer seu processamento e depois sinalizar a condição.
fonte
Fiz um exercício em sala de aula se você quiser um exemplo real da variável de condição:
fonte
Parece ser uma decisão de design específica e não uma necessidade conceitual.
De acordo com os pthreads, o motivo pelo qual o mutex não foi separado é que há uma melhora significativa no desempenho combinando-os e eles esperam que, devido às condições de corrida comuns, se você não usar um mutex, isso quase sempre será feito de qualquer maneira.
https://linux.die.net/man/3/pthread_cond_wait
fonte
Há toneladas de exegeses sobre isso, mas quero resumir isso com um exemplo a seguir.
O que há de errado com o trecho de código? Apenas pondere um pouco antes de prosseguir.
A questão é genuinamente sutil. Se o pai chama
thr_parent()
e avalia o valor dedone
, ele verá que é0
e, assim, tentará dormir. Mas, pouco antes de esperar para ir dormir, o pai é interrompido entre as linhas de 6 a 7 e a criança corre. O filho altera a variável de estadodone
para1
e sinaliza, mas nenhum thread está aguardando e, portanto, nenhum thread é ativado. Quando o pai corre novamente, dorme para sempre, o que é realmente flagrante.E se eles forem executados enquanto os bloqueios adquiridos individualmente?
fonte