Perché le funzioni delle variabili di condizione di pthreads richiedono un mutex?


182

Sto leggendo pthread.h; le funzioni relative alla variabile di condizione (come pthread_cond_wait(3)) richiedono un mutex come argomento. Perché? Per quanto posso dire, creerò un mutex solo per usare come argomento? Cosa dovrebbe fare quel mutex?

Risposte:


194

È solo il modo in cui le variabili di condizione sono (o sono state originariamente) implementate.

Il mutex viene utilizzato per proteggere la variabile condizione stessa . Ecco perché è necessario bloccarlo prima di aspettare.

L'attesa "atomicamente" sbloccherà il mutex, consentendo ad altri di accedere alla variabile di condizione (per la segnalazione). Quindi, quando la variabile condizione viene segnalata o trasmessa, uno o più thread nella lista di attesa verranno risvegliati e il mutex verrà nuovamente bloccato magicamente per quel thread.

In genere viene visualizzata la seguente operazione con variabili di condizione, illustrando come funzionano. L'esempio seguente è un thread di lavoro a cui viene dato lavoro tramite un segnale a una variabile di condizione.

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.

Il lavoro viene svolto all'interno di questo ciclo, a condizione che ce ne siano alcuni disponibili quando ritorna l'attesa. Quando il thread è stato contrassegnato per smettere di funzionare (di solito da un altro thread che imposta la condizione di uscita quindi calciare la variabile di condizione per riattivare questo thread), il loop uscirà, il mutex verrà sbloccato e questo thread uscirà.

Il codice sopra è un modello per singolo consumatore poiché il mutex rimane bloccato durante il lavoro. Per una variante multi-consumatore, è possibile utilizzare, come esempio :

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.

che consente ad altri consumatori di ricevere lavoro mentre questo fa lavoro.

La variabile condition ti alleggerisce dall'onere del polling di una condizione invece di consentire a un altro thread di avvisarti quando qualcosa deve accadere. Un altro thread può dire che il thread che funziona è disponibile come segue:

lock mutex.
flag work as available.
signal condition variable.
unlock mutex.

La stragrande maggioranza di quelli che sono spesso chiamati erroneamente risvegli spuri era generalmente sempre perché all'interno della loro pthread_cond_waitchiamata (trasmissione) erano stati segnalati più thread , si tornava con il mutex, si faceva il lavoro, quindi si aspettava di nuovo.

Quindi il secondo thread segnalato potrebbe uscire quando non c'era lavoro da fare. Quindi dovevi avere una variabile in più che indica che il lavoro doveva essere svolto (questo era intrinsecamente protetto da mutex con la coppia condvar / mutex qui - altri thread dovevano bloccare il mutex prima di cambiarlo comunque).

E ' stato tecnicamente possibile per un filo di tornare da una condizione di attesa senza essere preso a calci da un altro processo (si tratta di una vera e propria sveglia spurio), ma, in tutti i miei molti anni a lavorare su pthreads, sia in fase di sviluppo / di servizio del codice e come utente di loro, non ho mai ricevuto una di queste. Forse era solo perché HP aveva un'implementazione decente :-)

In ogni caso, lo stesso codice che gestiva il caso errato gestiva anche autentici risvegli spuri e poiché la bandiera disponibile per il lavoro non sarebbe stata impostata per quelli.


3
'fare qualcosa' non dovrebbe essere all'interno del ciclo while. Vorresti che il tuo ciclo while controllasse solo la condizione, altrimenti potresti anche 'fare qualcosa' se ricevi un risveglio spurio.
nn.

1
no, la gestione degli errori è seconda a questo. Con pthreads puoi essere svegliato, senza una ragione apparente (un risveglio spurio) e senza alcun errore. Pertanto, è necessario ricontrollare "alcune condizioni" dopo essersi svegliati.
nn.

1
Non sono sicuro di capire. Ho avuto la stessa reazione di nn ; perché è do somethingdentro il whileciclo?
ELLIOTTCABLE

1
Forse non lo sto chiarendo abbastanza. Il ciclo non è aspettare che il lavoro sia pronto, quindi puoi farlo. Il ciclo è il principale ciclo di lavoro "infinito". Se ritorni da cond_wait e il flag di lavoro è impostato, esegui il lavoro, quindi esegui nuovamente il ciclo. "while some condition" sarà falso solo quando vuoi che il thread smetta di funzionare a quel punto rilascerà il mutex e molto probabilmente uscirà.
paxdiablo,

7
@stefaanv "il mutex deve ancora proteggere la variabile di condizione, non esiste altro modo per proteggerlo": il mutex non deve proteggere la variabile di condizione; è per proteggere i dati del predicato , ma penso che tu sappia che dalla lettura del tuo commento che è seguito a tale affermazione. Puoi segnalare una variabile di condizione legalmente e pienamente supportata da implementazioni, post- sblocco del mutex che avvolge il predicato, e infatti in alcuni casi allevierai la contesa nel farlo.
WhozCraig,

59

Una variabile di condizione è piuttosto limitata se si potesse solo segnalare una condizione, di solito è necessario gestire alcuni dati relativi alla condizione segnalata. La segnalazione / il risveglio devono essere effettuati atomicamente per ottenere ciò senza introdurre condizioni di gara o essere eccessivamente complessi

pthreads può anche darti, per motivi piuttosto tecnici, un risveglio spurio . Ciò significa che devi controllare un predicato, quindi puoi essere sicuro che la condizione sia stata effettivamente segnalata e distinguerla da un risveglio spurio. Il controllo di una tale condizione per quanto riguarda l'attesa deve essere protetto - quindi una variabile di condizione ha bisogno di un modo per attendere / svegliarsi atomicamente mentre si blocca / sblocca un mutex che protegge tale condizione.

Considera un semplice esempio in cui ti viene notificato che alcuni dati vengono prodotti. Forse un altro thread ha creato alcuni dati desiderati e ha impostato un puntatore su tali dati.

Immagina un thread del produttore che fornisce alcuni dati a un altro thread del consumatore tramite un puntatore "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);
}

otterresti naturalmente molte condizioni di gara, cosa succederebbe se l'altro thread facesse some_data = new_datasubito dopo che ti sei svegliato, ma prima che lo facessidata = some_data

Non puoi davvero creare il tuo mutex per proteggere questo 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);
}

Non funzionerà, c'è ancora una possibilità di una condizione di gara tra svegliarsi e afferrare il mutex. Posizionare il mutex prima di pthread_cond_wait non ti aiuta, dato che ora manterrai il mutex durante l'attesa, ovvero il produttore non sarà mai in grado di afferrare il mutex. (nota, in questo caso potresti creare una seconda variabile di condizione per segnalare al produttore che hai finito some_data- anche se questo diventerà complesso, specialmente se vuoi molti produttori / consumatori.)

Quindi hai bisogno di un modo per rilasciare / afferrare atomicamente il mutex quando aspetti / ti svegli dalla condizione. Ecco cosa fanno le variabili di condizione pthread, ed ecco cosa faresti:

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);
}

(il produttore dovrebbe naturalmente prendere le stesse precauzioni, proteggendo sempre 'some_data' con lo stesso mutex e assicurandosi che non sovrascriva some_data se some_data è attualmente! = NULL)


Non dovrebbe while (some_data != NULL)essere un ciclo do-while in modo che aspetti la variabile di condizione almeno una volta?
Giudice Maygarden,

3
No. Quello che stai davvero aspettando, è che 'some_data' sia non nullo. Se la prima volta non è nulla, va bene, stai tenendo il mutex e puoi tranquillamente usare i dati. Se avessi un ciclo do / while mancherai la notifica se qualcuno segnalasse la variabile condizione prima di aspettare su di essa (non è niente come gli eventi trovati su win32 che rimangono segnalati fino a quando qualcuno li aspetta)
nn

4
Mi sono appena imbattuto in questa domanda e, francamente, è strano scoprire che questa risposta, che è semplicemente corretta, ha molti meno punti della risposta di paxdiablo che ha difetti definiti (l'atomicità è ancora necessaria, il mutex è necessario solo per gestire la condizione, non per la gestione o la notifica). Immagino sia così che funziona StackOverflow ...
stefaanv,

@stefaanv, se desideri dettagliare i difetti, come commenti alla mia risposta, così li vedo in modo tempestivo, piuttosto che mesi dopo :-), sarò felice di risolverli. Le tue brevi frasi non mi danno abbastanza dettagli per capire cosa stai cercando di dire.
paxdiablo,

1
@nos, non dovrebbe while(some_data != NULL)essere while(some_data == NULL)?
Eric Z,

30

Le variabili di condizione POSIX sono senza stato. Quindi è tua responsabilità mantenere lo stato. Poiché lo stato sarà accessibile sia dai thread in attesa che dai thread che indicano agli altri thread di interrompere l'attesa, deve essere protetto da un mutex. Se pensi di poter utilizzare le variabili di condizione senza un mutex, non hai capito che le variabili di condizione sono senza stato.

Le variabili di condizione sono costruite attorno a una condizione. I thread che attendono su una variabile di condizione attendono alcune condizioni. I thread che segnalano le variabili di condizione cambiano tale condizione. Ad esempio, un thread potrebbe essere in attesa dell'arrivo di alcuni dati. Qualche altro thread potrebbe notare che i dati sono arrivati. "I dati sono arrivati" è la condizione.

Ecco il classico uso di una variabile di condizione, semplificata:

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
}

Guarda come il thread è in attesa di lavoro. L'opera è protetta da un mutex. L'attesa rilascia il mutex in modo che un altro thread possa dare a questo thread un po 'di lavoro. Ecco come sarebbe segnalato:

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);
}

Si noti che è necessario il mutex per proteggere la coda di lavoro. Si noti che la variabile condizione stessa non ha idea se c'è lavoro o no. Cioè, una variabile di condizione deve essere associata a una condizione, tale condizione deve essere gestita dal codice e, poiché è condivisa tra i thread, deve essere protetta da un mutex.


1
Oppure, per dirla in modo più conciso, l'intero punto delle variabili di condizione è fornire un'operazione atomica di "sblocco e attesa". Senza un mutex, non ci sarebbe nulla da sbloccare.
David Schwartz,

Ti dispiacerebbe spiegare il significato di apolide ?
sn

@snr Non hanno alcun stato. Non sono "bloccati" o "segnalati" o "non firmati". Quindi è tua responsabilità tenere traccia di qualunque stato sia associato alla variabile condition. Ad esempio, se la variabile condizione fa sapere a un thread quando una coda diventa non vuota, deve essere possibile che un thread possa rendere la coda non vuota e alcuni altri thread devono sapere quando la coda diventa non vuota. Questo è uno stato condiviso e devi proteggerlo con un mutex. È possibile utilizzare la variabile condition, in associazione con quello stato condiviso protetto da un mutex, come meccanismo di riattivazione.
David Schwartz,

16

Non tutte le funzioni variabili di condizione richiedono un mutex: lo fanno solo le operazioni di attesa. Le operazioni di segnale e trasmissione non richiedono un mutex. Inoltre, una variabile di condizione non è associata in modo permanente a uno specifico mutex; il mutex esterno non protegge la variabile di condizione. Se una variabile di condizione ha uno stato interno, come una coda di thread in attesa, questa deve essere protetta da un blocco interno all'interno della variabile di condizione.

Le operazioni di attesa riuniscono una variabile di condizione e un mutex, perché:

  • un thread ha bloccato il mutex, ha valutato alcune espressioni su variabili condivise e lo ha trovato falso, in modo tale che deve attendere.
  • il thread deve spostarsi atomicamente dal possedere il mutex all'attesa della condizione.

Per questo motivo, l'operazione di attesa prende come argomenti sia il mutex che la condizione: in modo che possa gestire il trasferimento atomico di un thread dal possesso del mutex all'attesa, in modo che il thread non cada vittima della condizione di gara di sveglia persa .

Si verificherà una condizione di corsa di wakeup persa se un thread rinuncia a un mutex e quindi attende un oggetto di sincronizzazione senza stato, ma in un modo che non è atomico: esiste una finestra temporale in cui il thread non ha più il blocco e ha non ancora iniziato ad aspettare sull'oggetto. Durante questa finestra, può entrare un altro thread, rendere vera la condizione attesa, segnalare la sincronizzazione senza stato e quindi scomparire. L'oggetto senza stato non ricorda che è stato segnalato (è senza stato). Quindi il thread originale va in stop sull'oggetto di sincronizzazione stateless e non si riattiva, anche se la condizione di cui ha bisogno è già diventata vera: wakeup perso.

Le funzioni di attesa della variabile di condizione evitano il risveglio perso assicurandosi che il thread chiamante sia registrato per catturare in modo affidabile il risveglio prima che rinunci al mutex. Ciò sarebbe impossibile se la funzione di attesa della variabile di condizione non prendesse il mutex come argomento.


Potresti fornire un riferimento che le operazioni di trasmissione non richiedono l'acquisizione del mutex? Su MSVC la trasmissione viene ignorata.
xvan

@xvan Il POSIX pthread_cond_broadcaste le pthread_cond_signaloperazioni (di cui tratta questa domanda SO) non prendono nemmeno il mutex come argomento; solo la condizione. Le specifiche POSIX sono qui . Il mutex è menzionato solo in riferimento a ciò che accade nei thread in attesa quando si svegliano.
Kaz

Ti dispiacerebbe spiegare il significato di apolide ?
sn

1
@snr Un oggetto di sincronizzazione senza stato non ricorda alcuno stato relativo alla segnalazione. Quando viene segnalato, se qualcosa lo sta aspettando ora, viene svegliato, altrimenti la sveglia viene dimenticata. Le variabili di condizione sono apolidi in questo modo. Lo stato necessario per rendere affidabile la sincronizzazione viene gestito dall'applicazione e protetto dal mutex utilizzato in combinazione con le variabili di condizione, secondo la logica scritta correttamente.
Kaz

7

Non trovo le altre risposte concise e leggibili come questa pagina . Normalmente il codice di attesa è simile al seguente:

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

Esistono tre motivi per avvolgere il file wait()in un mutex:

  1. senza un mutex un altro thread potrebbe signal()prima del wait()e ci mancherebbe questo risveglio.
  2. normalmente check()dipende dalla modifica da un altro thread, quindi è comunque necessario escludere reciprocamente.
  3. per garantire che il thread con la priorità più alta proceda per primo (la coda per il mutex consente allo scheduler di decidere chi passa dopo).

Il terzo punto non è sempre un problema: il contesto storico è collegato dall'articolo a questa conversazione .

I risvegli spuri sono spesso citati riguardo a questo meccanismo (cioè il thread di attesa viene risvegliato senza signal()essere chiamato). Tuttavia, tali eventi sono gestiti dal loop check().


4

Le variabili di condizione sono associate a un mutex perché è l'unico modo in cui può evitare la razza che è progettata per evitare.

// 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!

A questo punto, non c'è nessun thread che segnalerà la variabile di condizione, quindi thread1 attenderà per sempre, anche se il metodo protectedReadyToRunVariable dice che è pronto per iniziare!

L'unico modo per aggirare questo problema è che le variabili di condizione rilascino atomicamente il mutex mentre contemporaneamente iniziano ad attendere la variabile di condizione. Questo è il motivo per cui la funzione cond_wait richiede un 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

Il mutex dovrebbe essere bloccato quando chiami pthread_cond_wait; quando lo chiami atomicamente entrambi sblocca il mutex e quindi blocca la condizione. Una volta segnalata la condizione, la blocca nuovamente atomicamente e ritorna.

Ciò consente l'implementazione di una pianificazione prevedibile, se lo si desidera, in quanto il thread che farebbe la segnalazione può attendere fino al rilascio del mutex per eseguire la sua elaborazione e quindi segnalare la condizione.


Quindi ... c'è una ragione per me non solo lasciare il mutex sempre sbloccato, e poi bloccarlo subito prima di aspettare, e poi sbloccarlo subito dopo aver terminato l'attesa?
ELLIOTTCABLE

Il mutex risolve anche alcune potenziali razze tra i thread di attesa e di segnalazione. fintanto che il mutex è sempre bloccato quando si cambiano le condizioni e la segnalazione, non ti troverai mai a mancare il segnale e dormire per sempre
Hasturkun

Quindi ... dovrei prima aspettare sul mutex sul conditionvar del conditionvar, prima di aspettare sul conditionvar? Non sono sicuro di aver capito affatto.
ELLIOTTCABLE

2
@elliottcable: senza tenere il mutex, come puoi sapere se dovresti o non dovresti aspettare? E se quello che stai aspettando fosse appena successo?
David Schwartz,

1

Ho fatto un esercizio in classe se vuoi un vero esempio di variabile di condizione:

#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

Sembra essere una decisione progettuale specifica piuttosto che un'esigenza concettuale.

Secondo i documenti di pthreads il motivo per cui il mutex non è stato separato è che c'è un significativo miglioramento delle prestazioni combinandoli e si aspettano che a causa delle condizioni di gara comuni se non si utilizza un mutex, quasi sempre verrà fatto comunque.

https://linux.die.net/man/3/pthread_cond_wait

Caratteristiche dei mutex e delle variabili di condizione

Era stato suggerito che l'acquisizione e il rilascio del mutex fossero disaccoppiati dall'attesa condizione. Ciò è stato respinto perché è la natura combinata dell'operazione che, di fatto, facilita le implementazioni in tempo reale. Tali implementazioni possono spostare atomicamente un thread ad alta priorità tra la variabile condition e il mutex in modo trasparente per il chiamante. Ciò può impedire ulteriori cambi di contesto e fornire un'acquisizione più deterministica di un mutex quando viene segnalato il thread in attesa. Pertanto, l'equità e le questioni prioritarie possono essere affrontate direttamente dalla disciplina di programmazione. Inoltre, l'operazione di attesa della condizione corrente corrisponde alla pratica esistente.


0

Ci sono un sacco di esegesi al riguardo, eppure voglio riassumerlo con un esempio che segue.

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 }

Cosa c'è di sbagliato nello snippet di codice? Rifletti un po 'prima di andare avanti.


Il problema è davvero sottile. Se il genitore invoca thr_parent()e quindi controlla il valore di done, vedrà che lo è 0e quindi proverà ad andare a dormire. Ma appena prima di chiamare aspetta di andare a dormire, il genitore viene interrotto tra le linee di 6-7 e il bambino corre. Il bambino cambia la variabile di stato done in 1e segnala, ma nessun thread è in attesa e quindi nessun thread viene svegliato. Quando il genitore corre di nuovo, dorme per sempre, il che è davvero egregio.

Che cosa succede se vengono eseguiti mentre i blocchi acquisiti singolarmente?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.