Por que pthread_cond_wait tem despertares espúrios?


145

Para citar a página do manual:

Ao usar variáveis ​​de condição, sempre há um predicado booleano que envolve variáveis ​​compartilhadas associadas a cada espera de condição que é verdadeira se o encadeamento continuar. Ativações espúrias das funções pthread_cond_timedwait () ou pthread_cond_wait () podem ocorrer. Como o retorno de pthread_cond_timedwait () ou pthread_cond_wait () não implica nada sobre o valor desse predicado, o predicado deve ser reavaliado com esse retorno.

Portanto, você pthread_cond_waitpode retornar mesmo que não tenha sinalizado. À primeira vista, pelo menos, isso parece bastante atroz. Seria como uma função que retornasse aleatoriamente o valor errado ou retornasse aleatoriamente antes de realmente atingir uma declaração de retorno adequada. Parece um grande erro. Mas o fato de terem optado por documentar isso na página de manual, em vez de corrigi-lo, parece indicar que há uma razão legítima pela qualpthread_cond_wait acorde espúrio. Presumivelmente, há algo intrínseco sobre como funciona que faz com que isso não possa ser ajudado. A questão é o que.

Por que pthread_cond_waitretornar espuriosamente? Por que não pode garantir que só vai acordar quando for sinalizado corretamente? Alguém pode explicar a razão de seu comportamento espúrio?


5
Eu imagino que tenha algo a ver com retornar sempre que o processo captar um sinal. A maioria dos * nixes não reinicia uma chamada bloqueada depois que um sinal a interrompe; eles apenas configuram / retornam um código de erro que diz que ocorreu um sinal.
cHao

1
@cHao: embora observe que, como as variáveis ​​de condição têm outros motivos para despertares espúrios, manipular um sinal não é um erro para pthread_cond_(timed)wait: "Se um sinal for entregue ... o encadeamento continuará aguardando a variável de condição como se fosse interrompido ou retornará zero devido a despertar espúrio ". Outras funções de bloqueio indicam EINTRquando interrompidas por um sinal (por exemplo read) ou são necessárias para retomar (por exemplo pthread_mutex_lock). Portanto, se não houvesse outras razões para despertar espúrio, pthread_cond_waitpoderia ter sido definido como qualquer uma dessas.
21411 Steve Steveop

4
Um artigo relacionado no Wikipedia: Spurious wakeup
Palec


Muitas funções não podem executar completamente seu trabalho completamente (E / S interrompida) e as funções de observação podem receber eventos não como uma alteração em um diretório em que a alteração foi cancelada ou revertida. Qual é o problema?
curiousguy

Respostas:


77

A seguinte explicação é dada por David R. Butenhof em "Programando com Threads POSIX" (p. 80):

Ativações espúrias podem parecer estranhas, mas em alguns sistemas multiprocessadores, tornar a ativação de condições completamente previsível pode diminuir substancialmente todas as operações de variáveis ​​de condição.

Na discussão comp.programming.threads a seguir , ele expande o pensamento por trás do design:

Patrick Doyle escreveu: 
> No artigo, Tom Payne escreveu: 
Kaz Kylheku escreveu: 
>>: É assim porque as implementações às vezes não podem evitar a inserção 
>>: esses despertares espúrios; pode ser caro evitá-los.

>> Mas porque? Por que isso é tão difícil? Por exemplo, estamos falando sobre
>> situações em que uma espera atinge o tempo limite quando um sinal chega? 

> Você sabe, eu me pergunto se os designers de pthreads usavam lógica como esta: 
> os usuários das variáveis ​​de condição precisam verificar a condição na saída de qualquer maneira, 
> por isso não colocaremos nenhum ônus adicional sobre eles se permitirmos 
> despertares espúrios; e uma vez que é concebível que a permissão espúria
> as ativações podem tornar a implementação mais rápida, só pode ajudar se 
> permita-os. 

> Eles podem não ter nenhuma implementação específica em mente. 

Na verdade, você não está muito longe, exceto que não foi longe o suficiente. 

A intenção era forçar o código correto / robusto, exigindo loops de predicado. Isso foi
impulsionado pelo contingente acadêmico comprovadamente correto entre os "principais tópicos" 
grupo de trabalho, embora eu não ache que alguém realmente discorde da intenção 
uma vez que eles entenderam o que isso significava. 

Seguimos essa intenção com vários níveis de justificação. O primeiro foi que
"religiosamente", usando um loop, protege o aplicativo contra seus próprios defeitos 
práticas de codificação. A segunda era que não era difícil imaginar abstratamente
máquinas e código de implementação que poderiam explorar esse requisito para melhorar 
o desempenho das operações de espera de condição média, otimizando o 
mecanismos de sincronização. 
/ ------------------ [David.Buten ... @ compaq.com] ------------------ \ 
| Arquiteto de threads POSIX da Compaq Computer Corporation |
| Meu livro: http://www.awl.com/cseng/titles/0-201-63392-2/ |
\ ----- [http://home.earthlink.net/~anneart/family/dave.html] ----- / 


22
basicamente isso não diz nada. Nenhuma explicação é dada aqui, a não ser o pensamento inicial de que "isso pode tornar as coisas mais rápidas", mas ninguém sabe como ou se realmente faz.
Bogdan Ionitza

107

Há pelo menos duas coisas que "despertar espúrio" pode significar:

  • Um encadeamento bloqueado pthread_cond_waitpode retornar da chamada, mesmo que não tenha ocorrido nenhuma chamada pthread_call_signalou pthread_cond_broadcastcondição.
  • Um encadeamento bloqueado pthread_cond_waitretorna devido a uma chamada para pthread_cond_signalou pthread_cond_broadcast, no entanto, após recuperar o mutex, o predicado subjacente não é mais verdadeiro.

Mas o último caso pode ocorrer mesmo se a implementação da variável de condição não permitir o caso anterior. Considere uma fila do consumidor produtor e três threads.

  • O segmento 1 acabou de desenfileirar um elemento e lançou o mutex, e a fila agora está vazia. O thread está fazendo o que faz com o elemento adquirido em alguma CPU.
  • O encadeamento 2 tenta desenfileirar um elemento, mas encontra a fila vazia quando marcada sob o mutex, as chamadas pthread_cond_waite os blocos na chamada que aguardam sinal / transmissão.
  • O segmento 3 obtém o mutex, insere um novo elemento na fila, notifica a variável de condição e libera o bloqueio.
  • Em resposta à notificação do encadeamento 3, o encadeamento 2, que estava aguardando a condição, está programado para ser executado.
  • No entanto, antes que o encadeamento 2 consiga entrar na CPU e agarrar o bloqueio da fila, o encadeamento 1 conclui sua tarefa atual e retorna à fila para mais trabalho. Ele obtém o bloqueio da fila, verifica o predicado e descobre que há trabalho na fila. Ele passa a desenfileirar o item que o thread 3 inseriu, libera a trava e faz o que faz com o item que o thread 3 enfileirou.
  • O segmento 2 agora entra em uma CPU e obtém o bloqueio, mas quando verifica o predicado, descobre que a fila está vazia. O segmento 1 'roubou' o item, para que a ativação pareça falsa. O segmento 2 precisa aguardar a condição novamente.

Portanto, como você sempre precisa verificar o predicado em um loop, não faz diferença se as variáveis ​​de condição subjacentes podem ter outros tipos de ativação espúria.


23
sim. Essencialmente, é isso que acontece quando um evento é usado em vez de um mecanismo de sincronização com uma contagem. Infelizmente, parece que os semáforos POSIX (no Linux de qualquer maneira) também estão sujeitos a ativações spurius. Eu apenas acho um pouco estranho que uma falha fundamental da funcionalidade das primitivas de sincronização seja aceita como 'normal' e precise ser contornada no nível do usuário :( Presumivelmente, os desenvolvedores estariam prontos se uma chamada do sistema fosse documentada com uma seção 'Spurious segfault' ou, talvez 'Spurious conecta ao URL errado' ou 'Spurious abertura do arquivo errado'.
Martin James

2
O cenário mais comum de uma "ativação espúria" provavelmente é o efeito colateral de uma chamada para pthread_cond_broadcast (). Digamos que você tenha um pool de 5 threads, dois acordam para a transmissão e fazem o trabalho. Os outros três acordam e descobrem que o trabalho foi feito. Os sistemas com vários processadores também podem resultar em um sinal condicional que ativa vários threads por acidente. O código apenas verifica o predicado novamente, vê um estado inválido e volta a dormir. Nos dois casos, a verificação do predicado resolve o problema. IMO, em geral, os usuários não devem usar mutexes e condicionais POSIX brutos.
CubicleSoft

1
@MartinJames - Que tal o clássico EINTR "espúrio"? Concordo que testar constantemente o EINTR em um loop é um pouco chato e torna o código bastante feio, mas os desenvolvedores fazem isso de qualquer maneira para evitar quebras aleatórias.
CubicleSoft 23/05

2
@Yola Não, não pode, porque você deve bloquear um mutex em torno do pthread_cond_signal/broadcaste não poderá fazê-lo, até que o mutex seja desbloqueado ao chamar pthread_cond_wait.
a3f 13/12/2016

1
O exemplo desta resposta é muito realista e eu concordo que a verificação de predicados é uma boa idéia. No entanto, não foi possível corrigi-lo da mesma maneira, seguindo a etapa problemática "o thread 1 conclui sua tarefa atual e retorna à fila para mais trabalho" e substituindo-o por "o thread 1 conclui sua tarefa atual e volta a aguardar a variável de condição "? Isso eliminaria o modo de falha descrito na resposta e tenho certeza de que tornaria o código correto, na ausência de despertares espúrios . Existe alguma implementação real que produza despertos espúrios na prática?
Quuxplusone 4/07

7

A seção "Vários despertares por sinal de condição" em pthread_cond_signal possui um exemplo de implementação de pthread_cond_wait e pthread_cond_signal, que envolve despertares espúrios.


2
Eu acho que essa resposta está errada, na medida em que vai. A implementação de amostra nessa página possui uma implementação de "notificar um" que é equivalente a "notificar todos"; mas não parece gerar despertares realmente espúrios . A única maneira de ativar um encadeamento é por outro encadeamento que chama "notificar todos" ou por outro encadeamento que invoca a coisa-rotulada "notificar um", que é realmente "notificar tudo".
Quuxplusone 4/07

5

Embora eu não ache que foi considerado no momento do design, há uma razão técnica real: em combinação com o cancelamento de encadeamento, há condições sob as quais a opção de ativar "espuriosamente" pode ser absolutamente necessária, pelo menos, a menos que você está disposto a impor restrições muito fortes a que tipo de estratégias de implementação são possíveis.

O principal problema é que, se um encadeamento atua no cancelamento enquanto está bloqueado pthread_cond_wait, os efeitos colaterais devem ser como se não consumisse nenhum sinal na variável de condição. No entanto, é difícil (e altamente restritivo) garantir que você ainda não tenha consumido um sinal ao começar a agir no cancelamento e, nesse estágio, pode ser impossível "re-postar" o sinal na variável de condição, pois você pode estar em uma situação em que o chamador de pthread_cond_signaljá está justificado por ter destruído o condvar e liberado a memória em que residia.

A permissão para despertar espúrio oferece uma saída fácil. Em vez de continuar atuando no cancelamento quando chegar bloqueado em uma variável de condição, se você já tiver consumido um sinal (ou se quiser ser preguiçoso, não importa o quê), pode declarar que ocorreu uma ativação espúria, e volte com sucesso. Isso não interfere na operação do cancelamento, porque um chamador correto simplesmente age no cancelamento pendente na próxima vez que ele faz um loop e liga pthread_cond_waitnovamente.

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.