Esempio / tutorial su Mutex? [chiuso]


176

Sono nuovo nel multithreading e sto cercando di capire come funzionano i mutex. Ha fatto molto Googling ma ha lasciato alcuni dubbi su come funziona perché ho creato il mio programma in cui il blocco non funzionava.

Una sintassi assolutamente non intuitiva del mutex è pthread_mutex_lock( &mutex1 );, dove sembra che il mutex sia bloccato, quando quello che voglio davvero bloccare è un'altra variabile. Questa sintassi significa che il blocco di un mutex blocca una regione di codice fino a quando il mutex non viene sbloccato? Quindi come fanno i thread a sapere che la regione è bloccata? [ AGGIORNAMENTO: i thread sanno che la regione è bloccata, tramite Memory Fencing ]. E un fenomeno del genere non dovrebbe essere chiamato sezione critica? [ AGGIORNAMENTO: gli oggetti della sezione critica sono disponibili solo in Windows, dove gli oggetti sono più veloci dei mutex e sono visibili solo al thread che lo implementa. Altrimenti, la sezione critica si riferisce semplicemente all'area del codice protetta da un mutex ]

In breve, potresti aiutarmi con il programma di esempio mutex più semplice possibile e la spiegazione più semplice possibile sulla logica di come funziona? Sono sicuro che questo aiuterà molti altri neofiti.


2
Continuando a enfatizzare la necessità di un semplice tutorial (sia esso boost thread, tbb o pthreads): Esempi di confusione: 1. stackoverflow.com/questions/3528877/… 2. stackoverflow.com/questions/2979525/… 3. stackoverflow.com/questions/2095977/to-mutex-or-not-to-mutex 4. stackoverflow.com/questions/3931026/… 5. stackoverflow.com/questions/1525189/…
Nav

1
Non intendo questo in modo offensivo, ma ciò che il tuo ultimo commento mi suggerisce è che abbiamo bisogno di meno analogie e una migliore spiegazione tecnica di come funziona un mutex e perché ne abbiamo bisogno.
San Jacinto

@San: nessuna offesa presa :) I miei commenti avevano lo scopo di suggerire che un neofita poteva ottenere la spiegazione più breve e chiara dei mutex. Molte analogie potrebbero creare confusione per il principiante, quindi analogie diverse dovrebbero essere mantenute separatamente. L'intero motivo per cui ho pubblicato le domande e le risposte è perché, come principiante, ho trovato doloroso leggere lunghe spiegazioni ed esempi di codice. Non vorrei che nessun altro soffrisse.
Nav

2
@Cory: se questa risposta potesse essere migliorata, sarei felice di prendere i tuoi suggerimenti. Sono solo felice che molte altre persone lo abbiano trovato utile. Se non ti ha aiutato, ci sono anche risposte di altre persone che hanno indicato altri tutorial di mutex. Perché essere così negativo?
Nav

Risposte:


278

Ecco il mio umile tentativo di spiegare il concetto ai neofiti di tutto il mondo: (a versione con codice colore anche sul mio blog)

Molte persone corrono in una cabina telefonica solitaria (non hanno telefoni cellulari) per parlare con i loro cari. La prima persona a prendere la maniglia della porta è quella a cui è permesso usare il telefono. Deve tenere aggrappato alla maniglia della porta fintanto che usa il telefono, altrimenti qualcun altro afferrerà la maniglia, lo getterà fuori e parlerà con sua moglie :) Non esiste un sistema di code in quanto tale. Quando la persona termina la sua chiamata, esce dalla cabina e lascia la maniglia della porta, la persona successiva a prendere la maniglia della porta sarà autorizzata a utilizzare il telefono.

Un thread è: Ogni persona
Il mutex è: La maniglia della porta
La serratura è: La mano della persona
La risorsa è: Il telefono

Qualsiasi thread che deve eseguire alcune righe di codice che non devono essere modificate contemporaneamente da altri thread (usando il telefono per parlare con sua moglie), deve prima acquisire un blocco su un mutex (stringendo la maniglia della porta della cabina ). Solo così un thread sarà in grado di eseguire quelle righe di codice (effettuando la telefonata).

Una volta che il thread ha eseguito quel codice, dovrebbe rilasciare il blocco sul mutex in modo che un altro thread possa acquisire un blocco sul mutex (altre persone sono in grado di accedere alla cabina telefonica).

[ Il concetto di avere un mutex è un po 'assurdo quando si considera l'accesso esclusivo nel mondo reale, ma nel mondo della programmazione credo che non ci fosse altro modo per far vedere agli altri thread che un thread stava già eseguendo alcune righe di codice. Esistono concetti di mutex ricorsivi, ecc., Ma questo esempio era inteso solo a mostrarti il ​​concetto di base. Spero che l'esempio ti dia una chiara immagine del concetto. ]

Con threading C ++ 11:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Compilare ed eseguire utilizzando g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Invece di usare esplicitamente locke unlock, puoi usare le parentesi come mostrato qui , se stai usando un blocco con ambito per il vantaggio che offre . I blocchi con ambito hanno tuttavia un leggero sovraccarico di prestazioni.


2
@San: sarò sincero; Sì, mi piace il fatto che tu abbia fatto del tuo meglio per spiegare i dettagli (con flusso) a un principiante completo. MA, (per favore, non fraintendetemi), l'intenzione di questo post era di mettere il concetto in una breve spiegazione (perché le altre risposte indicavano lunghi tutorial). Spero non ti dispiaccia se ti chiedo di copiare l'intera risposta e di pubblicarla come risposta separata? In modo da poter eseguire il rollback e modificare la mia risposta per puntare alla tua risposta.
Nav

2
@Tom In tal caso, non dovresti accedere a quel mutex. Le operazioni su di esso dovrebbero essere incapsulate in modo tale da proteggere qualsiasi cosa stia proteggendo da tali sciocchezze. Se quando si utilizza l'API esposta della libreria, la libreria è garantita come thread-safe, allora si è sicuri di includere un mutex nettamente diverso per proteggere i propri elementi condivisi. Altrimenti, stai davvero aggiungendo una nuova maniglia, come hai suggerito.
San Jacinto

2
Per estendere il mio punto, quello che vorresti fare è aggiungere un'altra stanza più grande attorno allo stand. La camera può contenere anche servizi igienici e doccia. Diciamo che è consentita 1 sola persona nella stanza alla volta. È necessario progettare la stanza in modo che questa stanza abbia una porta con una maniglia che protegga l'ingresso nella stanza proprio come la cabina telefonica. Quindi ora, anche se hai altri mutex, puoi riutilizzare la cabina telefonica in qualsiasi progetto. Un'altra opzione sarebbe quella di esporre meccanismi di blocco per ciascun dispositivo nella stanza e gestire i blocchi nella classe della stanza. Ad ogni modo, non aggiungerei nuovi blocchi allo stesso oggetto.
San Jacinto

8
Il tuo esempio di threading C ++ 11 è errato . Quindi è quello TBB, l'indizio è nel blocco con ambito nome .
Jonathan Wakely,

3
Sono ben consapevole di entrambi, @Jonathan. Sembra che ti sia sfuggita la frase che ho scritto (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Per quanto riguarda l'utilizzo del blocco con ambito, dipende dallo sviluppatore, a seconda del tipo di applicazione che stanno costruendo. Questa risposta aveva lo scopo di indirizzare la comprensione di base del concetto di mutex e non di entrare in tutte le complessità di esso, quindi i tuoi commenti e link sono i benvenuti ma un po 'fuori portata di questo tutorial.
Nav

41

Mentre un mutex può essere usato per risolvere altri problemi, il motivo principale per cui esistono è quello di fornire l'esclusione reciproca e quindi risolvere quella che è conosciuta come una condizione di razza. Quando due (o più) thread o processi stanno tentando di accedere contemporaneamente alla stessa variabile, abbiamo il potenziale per una condizione di competizione. Considera il seguente codice

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

Gli interni di questa funzione sembrano così semplici. È solo un'affermazione. Tuttavia, un tipico equivalente del linguaggio pseudoassemblaggio potrebbe essere:

load i from memory into a register
add 1 to i
store i back into memory

Poiché per eseguire l'operazione di incremento su i sono necessarie tutte le istruzioni equivalenti in linguaggio assembly, diciamo che incrementare i è un'operazione non atmoica. Un'operazione atomica può essere completata sull'hardware con una garanzia di non essere interrotta una volta iniziata l'esecuzione dell'istruzione. L'aumento i consiste in una catena di 3 istruzioni atomiche. In un sistema concorrente in cui più thread chiamano la funzione, sorgono problemi quando un thread legge o scrive nel momento sbagliato. Immagina di avere due thread in esecuzione simultanea e uno chiama la funzione immediatamente dopo l'altro. Diciamo anche che abbiamo inizializzato a 0. Supponiamo anche che abbiamo molti registri e che i due thread utilizzino registri completamente diversi, quindi non ci saranno collisioni. I tempi effettivi di questi eventi possono essere:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

Quello che è successo è che abbiamo due thread che incrementano contemporaneamente, la nostra funzione viene chiamata due volte, ma il risultato non è coerente con questo fatto. Sembra che la funzione sia stata chiamata una sola volta. Questo perché l'atomicità è "rotta" a livello di macchina, il che significa che i thread possono interrompersi a vicenda o lavorare insieme nei momenti sbagliati.

Abbiamo bisogno di un meccanismo per risolvere questo. Dobbiamo imporre alcuni ordini alle istruzioni sopra. Un meccanismo comune è quello di bloccare tutti i thread tranne uno. Pthread mutex utilizza questo meccanismo.

Qualsiasi thread che deve eseguire alcune righe di codice che possono modificare in modo non sicuro valori condivisi da altri thread contemporaneamente (usando il telefono per parlare con sua moglie), dovrebbe prima essere fatto acquisire un blocco su un mutex. In questo modo, qualsiasi thread che richiede l'accesso ai dati condivisi deve passare attraverso il blocco mutex. Solo allora un thread sarà in grado di eseguire il codice. Questa sezione del codice è chiamata sezione critica.

Una volta eseguita la sezione critica, il thread dovrebbe rilasciare il blocco sul mutex in modo che un altro thread possa acquisire un blocco sul mutex.

Il concetto di avere un mutex sembra un po 'strano quando si considerano gli umani che cercano un accesso esclusivo a oggetti fisici reali, ma durante la programmazione, dobbiamo essere intenzionali. Discussioni e processi concorrenti non hanno l'educazione sociale e culturale che facciamo, quindi dobbiamo costringerli a condividere i dati in modo corretto.

Tecnicamente parlando, come funziona un mutex? Non soffre delle stesse condizioni di gara che abbiamo menzionato prima? Pthread_mutex_lock () non è forse un po 'più complesso di un semplice incremento di una variabile?

Tecnicamente parlando, abbiamo bisogno di supporto hardware per aiutarci. I progettisti hardware ci danno istruzioni sulla macchina che fanno più di una cosa ma sono garantite per essere atomiche. Un classico esempio di tale istruzione è il test-and-set (TAS). Quando si tenta di acquisire un blocco su una risorsa, è possibile che TAS verifichi se un valore in memoria è 0. In caso affermativo, questo sarebbe il nostro segnale che la risorsa è in uso e non facciamo nulla (o più accuratamente , attendiamo un meccanismo. Un mutex di pthreads ci metterà in una coda speciale nel sistema operativo e ci avviserà quando la risorsa sarà disponibile. I sistemi più stupidi potrebbero richiedere di eseguire un loop spin stretto, testando la condizione più e più volte) . Se il valore in memoria non è 0, TAS imposta la posizione su un valore diverso da 0 senza utilizzare altre istruzioni. E' è come combinare due istruzioni di assemblaggio in 1 per darci atomicità. Pertanto, una volta iniziato, il test e la modifica del valore (se la modifica è appropriata) non possono essere interrotti. Possiamo costruire mutex sopra una simile istruzione.

Nota: alcune sezioni potrebbero apparire simili a una risposta precedente. Ho accettato il suo invito a modificare, ha preferito l'originale, quindi sto mantenendo ciò che avevo che è infuso con un po 'della sua verbosità.


1
Grazie mille, San. Ho collegato alla tua risposta :) In realtà, avevo intenzione di prendere la mia risposta + la tua risposta e pubblicarla come risposta separata, per mantenere il flusso. Non mi dispiace davvero se riutilizzi qualsiasi parte della mia risposta. Non lo facciamo comunque per noi stessi.
Nav

13

Il miglior tutorial sui thread che conosco è qui:

https://computing.llnl.gov/tutorials/pthreads/

Mi piace il fatto che sia scritto sull'API, piuttosto che su un'implementazione particolare, e fornisce alcuni semplici esempi per aiutarti a capire la sincronizzazione.


Sono d'accordo che è sicuramente un buon tutorial, ma ci sono molte informazioni su una singola pagina e i programmi sono lunghi. La domanda che ho pubblicato è la versione mutex del discorso "I have a dream", in cui i neofiti troverebbero un modo semplice per conoscere i mutex e capire come funziona la sintassi non intuitiva (questa è una spiegazione che manca in tutti i tutorial) .
Nav

7

Mi sono imbattuto in questo post di recente e penso che abbia bisogno di una soluzione aggiornata per il mutex c ++ 11 della libreria standard (vale a dire std :: mutex).

Ho incollato un po 'di codice qui sotto (i miei primi passi con un mutex - ho imparato la concorrenza su win32 con HANDLE, SetEvent, WaitForMultipleObjects ecc.).

Dal momento che è il mio primo tentativo con std :: mutex e amici, mi piacerebbe vedere commenti, suggerimenti e miglioramenti!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}

1
Super! Grazie per la pubblicazione. Anche se, come ho già detto, il mio scopo era semplicemente quello di spiegare il concetto di mutex. Tutti gli altri tutorial hanno reso molto difficile l'aggiunta dei concetti di consumo del produttore, variabili di condizione ecc., Il che mi ha reso molto difficile capire cosa diavolo stava succedendo.
Nav

4

La funzione pthread_mutex_lock()sia acquisisce il mutex per il thread chiamante o blocca il filo fino alla mutex può essere acquisita. Il relativo pthread_mutex_unlock()rilascia il mutex.

Pensa al mutex come a una coda; ogni thread che tenta di acquisire il mutex verrà posizionato alla fine della coda. Quando un thread rilascia il mutex, il thread successivo nella coda si stacca e ora è in esecuzione.

Una sezione critica si riferisce a una regione di codice in cui è possibile il non determinismo. Spesso perché più thread stanno tentando di accedere a una variabile condivisa. La sezione critica non è sicura fino a quando non è in atto una sorta di sincronizzazione. Un blocco mutex è una forma di sincronizzazione.


1
È garantito che entrerà esattamente il prossimo thread di tentativi?
Arsen Mkrtchyan,

1
@Arsen Nessuna garanzia. È solo un'analogia utile.
Chrisaycock,

3

Dovresti controllare la variabile mutex prima di usare l'area protetta da mutex. Quindi il tuo pthread_mutex_lock () potrebbe (a seconda dell'implementazione) attendere fino al rilascio di mutex1 o restituire un valore che indica che non è possibile ottenere il blocco se qualcun altro lo ha già bloccato.

Il mutex è davvero solo un semaforo semplificato. Se leggi su di loro e li capisci, capisci i mutex. Esistono diverse domande relative a mutex e semafori nella SO. Differenza tra semaforo binario e mutex , quando dovremmo usare il mutex e quando dovremmo usare il semaforo e così via. L'esempio della toilette nel primo link è un esempio altrettanto valido di quanto si possa pensare. Tutto il codice fa è verificare se la chiave è disponibile e se lo è, la riserva. Si noti che non si riserva il bagno stesso, ma la chiave.


1
pthread_mutex_locknon può tornare se qualcun altro detiene il blocco. In questo caso si blocca e questo è il punto. pthread_mutex_trylockè la funzione che verrà restituita se si tiene premuto il blocco.
R .. GitHub smette di aiutare ICE il

1
Sì, all'inizio non avevo capito di che implementazione si tratta.
Makis

3

Per coloro che cercano l'esempio di mutex di shortex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}

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.