Por que as funções de variável de condição do pthreads exigem um mutex?


182

Eu estou lendo pthread.h; as funções relacionadas à variável de condição (como pthread_cond_wait(3)) requerem um mutex como argumento. Por quê? Até onde eu sei, vou criar um mutex apenas para usar como esse argumento? O que esse mutex deveria fazer?

Respostas:


194

É 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_waitchamada (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.


3
'fazer alguma coisa' não deve estar dentro do loop while. Você deseja que o seu loop while verifique apenas a condição, caso contrário, você também pode "fazer alguma coisa" se receber uma ativação espúria.
nºs

1
não, o tratamento de erros é o segundo disso. Com pthreads, você pode ser acordado, sem motivo aparente (uma ativação espúria) e sem nenhum erro. Portanto, você precisa verificar novamente 'alguma condição' depois de acordar.
nºs

1
Eu não tenho certeza se entendi. Eu tive a mesma reação que nos ; por que está do somethingdentro do whileloop?
ELLIOTTCABLE

1
Talvez eu não esteja deixando claro o suficiente. O loop é não esperar o trabalho estar pronto para que você possa fazê-lo. O loop é o principal loop de trabalho "infinito". Se você retornar de cond_wait e o sinalizador de trabalho estiver definido, o trabalho será executado novamente. "while some condition" somente será falso quando você desejar que o encadeamento pare de executar o trabalho; nesse ponto, ele liberará o mutex e provavelmente sairá.
21460

7
@stefaanv "o mutex ainda deve proteger a variável de condição, não há outra maneira de protegê-lo": o mutex não é para proteger a variável de condição; é para proteger os dados do predicado , mas acho que você sabe disso ao ler seu comentário que seguiu essa declaração. Você pode sinalizar uma variável de condição legalmente e totalmente suportada por implementações, pós- desbloqueio do mutex envolvendo o predicado e, de fato, aliviará a disputa ao fazê-lo em alguns casos.
WhozCraig

59

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

while(1) {
    pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
    char *data = some_data;
    some_data = NULL;
    handle(data);
}

você naturalmente teria muitas condições de corrida, e se o outro tópico tivesse ocorrido some_data = new_datalogo 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

while(1) {

    pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
    pthread_mutex_lock(&mutex);
    char *data = some_data;
    some_data = NULL;
    pthread_mutex_unlock(&mutex);
    handle(data);
}

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:

while(1) {
    pthread_mutex_lock(&mutex);
    while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also 
                               // make it robust if there were several consumers
       pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
    }

    char *data = some_data;
    some_data = NULL;
    pthread_mutex_unlock(&mutex);
    handle(data);
}

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


Não deveria while (some_data != NULL)ser um loop do-while para aguardar a variável de condição pelo menos uma vez?
Maygarden do juiz

3
Não. O que você está realmente esperando é que 'some_data' seja não nulo. Se não for nulo na "primeira vez", ótimo, você está segurando o mutex e pode usar os dados com segurança. Se você tivesse um loop do / while, perderia a notificação se alguém sinalizasse a variável de condição antes de esperá-la (não é como os eventos encontrados no win32 que permanecem sinalizados até que alguém espere por eles)

4
Eu apenas tropecei nessa questão e, francamente, é estranho descobrir que essa resposta, que é correta, tem muito menos pontos do que a resposta de paxdiablo, que possui falhas definidas (a atomicidade ainda é necessária, a mutex ainda é necessária para lidar com a condição, não para manipulação ou notificação). Eu acho que é apenas como funciona o stackoverflow ...
stefaanv

@stefaanv, se você quiser detalhar as falhas, como comentários à minha resposta, para vê-las em tempo hábil, em vez de meses depois :-), ficarei feliz em corrigi-las. Suas frases breves não me dão detalhes suficientes para entender o que você está tentando dizer.
precisa

1
@nos, não deveria while(some_data != NULL)ser while(some_data == NULL)?
Eric Z

30

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:

while(1)
{
    pthread_mutex_lock(&work_mutex);

    while (work_queue_empty())       // wait for work
       pthread_cond_wait(&work_cv, &work_mutex);

    work = get_work_from_queue();    // get work

    pthread_mutex_unlock(&work_mutex);

    do_work(work);                   // do that work
}

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:

void AssignWork(WorkItem work)
{
    pthread_mutex_lock(&work_mutex);

    add_work_to_queue(work);           // put work item on queue

    pthread_cond_signal(&work_cv);     // wake worker thread

    pthread_mutex_unlock(&work_mutex);
}

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.


1
Ou, para ser mais conciso, o ponto inteiro das variáveis ​​de condição é fornecer uma operação atômica de "desbloqueio e espera". Sem um mutex, não haveria nada para desbloquear.
David Schwartz

Você se importaria de explicar o significado de apátrida ?
snr

@snr Eles não têm nenhum estado. Eles não estão "bloqueados" ou "sinalizados" ou "não sinalizados". Portanto, é de sua responsabilidade acompanhar qualquer estado associado à variável de condição. Por exemplo, se a variável de condição permite que um encadeamento saiba quando uma fila fica vazia, deve ser o caso de um encadeamento tornar a fila não vazia e algum outro encadeamento precisa saber quando a fila fica vazia. Esse é o estado compartilhado e você deve protegê-lo com um mutex. Você pode usar a variável de condição, em associação com esse estado compartilhado protegido por um mutex, como o mecanismo de ativação.
David Schwartz

16

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:

  • um encadeamento bloqueou o mutex, avaliou alguma expressão sobre variáveis ​​compartilhadas e o considerou falso, de modo que ele precisa aguardar.
  • o encadeamento deve passar atomicamente de possuir o mutex, para aguardar a condição.

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.


Você poderia fornecer referência de que as operações de transmissão não exigem a aquisição do mutex? Na MSVC, a transmissão é ignorada.
xvan

@xvan O POSIX pthread_cond_broadcaste as pthread_cond_signaloperaçõ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.
Kaz

Você se importaria de explicar o significado de apátrida ?
snr

1
@snr Um objeto de sincronização sem estado não se lembra de nenhum estado relacionado à sinalização. Quando sinalizado, se algo está esperando agora, é acordado, caso contrário, o despertar é esquecido. Variáveis ​​de condição são sem estado como este. O estado necessário para tornar a sincronização confiável é mantido pelo aplicativo e protegido pelo mutex usado em conjunto com as variáveis ​​de condição, de acordo com a lógica escrita corretamente.
Kaz

7

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:

mutex.lock()
while(!check())
    condition.wait()
mutex.unlock()

Há três razões para agrupar o wait()em um mutex:

  1. sem um mutex, outro segmento poderia signal()antes do wait()e nós sentiríamos falta desse despertar.
  2. normalmente check()depende da modificação de outro encadeamento, portanto, você precisa de exclusão mútua nele.
  3. para garantir que o encadeamento de maior prioridade prossiga primeiro (a fila do mutex permite que o planejador decida quem será o próximo).

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 loop check().


4

Variáveis ​​de condição estão associadas a um mutex, porque é a única maneira de evitar a corrida que foi projetada para evitar.

// incorrect usage:
// thread 1:
while (notDone) {
    pthread_mutex_lock(&mutex);
    bool ready = protectedReadyToRunVariable
    pthread_mutex_unlock(&mutex);
    if (ready) {
        doWork();
    } else {
        pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
    }
}

// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
   protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);

Now, lets look at a particularly nasty interleaving of these operations

pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
                                 pthread_mutex_lock(&mutex);
                                 protectedReadyToRuNVariable = true;
                                 pthread_mutex_unlock(&mutex);
                                 pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!

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

// correct usage:
// thread 1:
while (notDone) {
    pthread_mutex_lock(&mutex);
    bool ready = protectedReadyToRunVariable
    if (ready) {
        pthread_mutex_unlock(&mutex);
        doWork();
    } else {
        pthread_cond_wait(&mutex, &cond1);
    }
}

// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
   protectedReadyToRuNVariable = true;
   pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);

3

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.


Então… existe uma razão para eu não apenas deixar o mutex sempre desbloqueado e depois trancá-lo antes de esperar e depois destrancar?
ELLIOTTCABLE

O mutex também resolve algumas possíveis corridas entre os segmentos de espera e sinalização. enquanto o mutex está sempre bloqueado ao alterar o estado e sinalização, você nunca vai encontrar-se perder o sinal e dormir para sempre
Hasturkun

Então ... eu deveria primeiro esperar no mutex no mutex da condição, antes de esperar na condição? Não tenho certeza se entendi.
ELLIOTTCABLE

2
@elliottcable: Sem segurar o mutex, como você poderia saber se deveria ou não esperar? E se o que você está esperando apenas aconteceu?
David Schwartz

1

Fiz um exercício em sala de aula se você quiser um exemplo real da variável de condição:

#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"

int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;

void attenteSeuil(arg)
{
    pthread_mutex_lock(&mutex_compteur);
        while(compteur < 10)
        {
            printf("Compteur : %d<10 so i am waiting...\n", compteur);
            pthread_cond_wait(&varCond, &mutex_compteur);
        }
        printf("I waited nicely and now the compteur = %d\n", compteur);
    pthread_mutex_unlock(&mutex_compteur);
    pthread_exit(NULL);
}

void incrementCompteur(arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex_compteur);

            if(compteur == 10)
            {
                printf("Compteur = 10\n");
                pthread_cond_signal(&varCond);
                pthread_mutex_unlock(&mutex_compteur);
                pthread_exit(NULL);
            }
            else
            {
                printf("Compteur ++\n");
                compteur++;
            }

        pthread_mutex_unlock(&mutex_compteur);
    }
}

int main(int argc, char const *argv[])
{
    int i;
    pthread_t threads[2];

    pthread_mutex_init(&mutex_compteur, NULL);

    pthread_create(&threads[0], NULL, incrementCompteur, NULL);
    pthread_create(&threads[1], NULL, attenteSeuil, NULL);

    pthread_exit(NULL);
}

1

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

Recursos de mutexes e variáveis ​​de condição

Foi sugerido que a aquisição e liberação do mutex sejam dissociadas da espera de condição. Isso foi rejeitado porque é a natureza combinada da operação que, de fato, facilita as implementações em tempo real. Essas implementações podem mover atomicamente um encadeamento de alta prioridade entre a variável de condição e o mutex de maneira transparente ao chamador. Isso pode impedir alternância extra de contexto e fornecer uma aquisição mais determinística de um mutex quando o segmento em espera é sinalizado. Assim, questões de justiça e prioridade podem ser tratadas diretamente pela disciplina de programação. Além disso, a operação de espera da condição atual corresponde à prática existente.


0

Há toneladas de exegeses sobre isso, mas quero resumir isso com um exemplo a seguir.

1 void thr_child() {
2    done = 1;
3    pthread_cond_signal(&c);
4 }

5 void thr_parent() {
6    if (done == 0)
7        pthread_cond_wait(&c);
8 }

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 de done, ele verá que é 0e, 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 estado donepara 1e 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?

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.