Mutex Beispiel / Tutorial? [geschlossen]


177

Ich bin neu im Multithreading und habe versucht zu verstehen, wie Mutexe funktionieren. Ich habe viel gegoogelt und ein anständiges Tutorial gefunden , aber es hat immer noch Zweifel daran gelassen, wie es funktioniert, weil ich mein eigenes Programm erstellt habe, in dem das Sperren nicht funktioniert hat.

Eine absolut nicht intuitive Syntax des Mutex ist pthread_mutex_lock( &mutex1 );, wo es so aussieht, als würde der Mutex gesperrt, wenn ich wirklich eine andere Variable sperren möchte. Bedeutet diese Syntax, dass das Sperren eines Mutex einen Codebereich sperrt, bis der Mutex entsperrt wird? Woher wissen Threads dann, dass die Region gesperrt ist? [ UPDATE: Threads wissen, dass die Region durch Memory Fencing gesperrt ist ]. Und soll ein solches Phänomen nicht als kritischer Abschnitt bezeichnet werden? [ UPDATE: Objekte mit kritischen Abschnitten sind nur in Windows verfügbar, wo die Objekte schneller als Mutexe sind und nur für den Thread sichtbar sind, der sie implementiert. Andernfalls bezieht sich der kritische Abschnitt nur auf den durch einen Mutex geschützten Codebereich. ]

Kurz gesagt, könnten Sie bitte mit einem möglichst einfachen Mutex- Beispielprogramm und einer möglichst einfachen Erklärung der Funktionsweise helfen ? Ich bin sicher, das wird vielen anderen Neulingen helfen .


2
Betonen Sie weiterhin die Notwendigkeit eines einfachen Tutorials (sei es Boost-Threads, TBB oder Pthreads): Beispiele für die Verwirrung: 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
Ich meine das nicht offensiv, aber Ihr letzter Kommentar legt mir nahe, dass wir weniger Analogien und eine bessere technische Erklärung brauchen, wie ein Mutex funktioniert und warum wir sie brauchen.
San Jacinto

@San: Keine Beleidigung :) Meine Kommentare sollten nur darauf hinweisen, dass ein Neuling die kürzeste und klarste Erklärung für Mutexe erhalten könnte. Viele Analogien könnten für den Neuling verwirrend werden, daher sollten verschiedene Analogien getrennt aufbewahrt werden. Der ganze Grund für mich, die Fragen und Antworten zu veröffentlichen, ist, dass ich es als Neuling als schwierig empfand, lange Erklärungen und Codebeispiele durchzulesen. Ich würde nicht wollen, dass jemand anderes durch den Schmerz geht.
Nav

2
@Cory: Wenn diese Antwort verbessert werden könnte, würde ich gerne Ihre Vorschläge annehmen. Ich bin nur froh, dass viele andere Leute es hilfreich fanden. Wenn es Ihnen nicht geholfen hat, gibt es auch Antworten von anderen Personen, die auf andere Mutex-Tutorials verwiesen haben. Warum so negativ sein?
Nav

Antworten:


277

Hier ist mein bescheidener Versuch, Neulingen auf der ganzen Welt das Konzept zu erklären: (eine farbcodierte Version auch in meinem Blog)

Viele Leute rennen zu einer einzelnen Telefonzelle (sie haben keine Handys), um mit ihren Lieben zu sprechen. Die erste Person, die den Türgriff der Kabine erfasst, ist die Person, die das Telefon benutzen darf. Er muss sich so lange am Türgriff festhalten, wie er das Telefon benutzt, sonst ergreift jemand anderes den Griff, wirft ihn raus und spricht mit seiner Frau :) Es gibt kein Warteschlangensystem als solches. Wenn die Person ihren Anruf beendet, aus der Kabine kommt und den Türgriff verlässt, darf die nächste Person, die den Türgriff ergreift, das Telefon benutzen.

Ein Thread ist: Jede Person
Der Mutex ist: Der Türgriff
Das Schloss ist: Die Hand der Person
Die Ressource ist: Das Telefon

Jeder Thread, der einige Codezeilen ausführen muss, die nicht gleichzeitig von anderen Threads geändert werden sollten (mit dem Telefon, um mit seiner Frau zu sprechen), muss zuerst ein Schloss für einen Mutex erwerben (den Türgriff der Kabine umklammern) ). Nur dann kann ein Thread diese Codezeilen ausführen (Telefonieren).

Sobald der Thread diesen Code ausgeführt hat, sollte er die Sperre für den Mutex aufheben, damit ein anderer Thread eine Sperre für den Mutex erhalten kann (andere Personen können auf die Telefonzelle zugreifen).

[ Das Konzept, einen Mutex zu haben, ist etwas absurd, wenn man den exklusiven Zugriff in der realen Welt betrachtet, aber in der Programmierwelt gab es wohl keine andere Möglichkeit, die anderen Threads "sehen" zu lassen, dass ein Thread bereits einige Codezeilen ausführte. Es gibt Konzepte für rekursive Mutexe usw., aber dieses Beispiel sollte Ihnen nur das Grundkonzept zeigen. Ich hoffe, das Beispiel gibt Ihnen ein klares Bild des Konzepts. ]]

Mit C ++ 11-Threading:

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

Kompilieren und ausführen mit g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Anstatt explizit lockund zu verwenden unlock, können Sie die hier gezeigten Klammern verwenden , wenn Sie eine Sperre mit Gültigkeitsbereich für den Vorteil verwenden, den sie bietet . Sperren mit Gültigkeitsbereich haben jedoch einen geringen Leistungsaufwand.


2
@ San: Ich werde ehrlich sein; Ja, ich mag die Tatsache, dass Sie Ihr Bestes gegeben haben, um einem vollständigen Neuling die Details (mit Fluss) zu erklären. ABER (bitte verstehe mich nicht falsch) die Absicht dieses Beitrags war es, das Konzept in eine kurze Erklärung zu bringen (da die anderen Antworten auf lange Tutorials verweisen). Ich hoffe, es würde Ihnen nichts ausmachen, wenn ich Sie auffordere, Ihre gesamte Antwort zu kopieren und als separate Antwort zu veröffentlichen. Damit ich meine Antwort zurücksetzen und bearbeiten kann, um auf Ihre Antwort zu verweisen.
Nav

2
@ Tom In diesem Fall sollten Sie nicht auf diesen Mutex zugreifen. Operationen darauf sollten gekapselt werden, damit alles, was es bewacht, vor solchen Dummheiten geschützt ist. Wenn Sie die exponierte API der Bibliothek verwenden, ist die Bibliothek garantiert threadsicher, und Sie können sicher einen deutlich anderen Mutex einfügen, um Ihre eigenen freigegebenen Elemente zu schützen. Andernfalls fügen Sie tatsächlich einen neuen Türgriff hinzu, wie Sie vorgeschlagen haben.
San Jacinto

2
Um meinen Standpunkt zu erweitern, möchten Sie einen weiteren, größeren Raum um den Stand herum hinzufügen. Das Zimmer kann auch eine Toilette und eine Dusche enthalten. Angenommen, nur 1 Person darf gleichzeitig den Raum betreten. Sie müssen den Raum so gestalten, dass dieser Raum eine Tür mit einem Griff hat, der den Zugang zum Raum ähnlich wie die Telefonzelle schützt. Obwohl Sie jetzt zusätzliche Mutexe haben, können Sie die Telefonzelle in jedem Projekt wiederverwenden. Eine andere Möglichkeit wäre, Sperrmechanismen für jedes Gerät im Raum freizulegen und die Schlösser in der Raumklasse zu verwalten. In beiden Fällen würden Sie demselben Objekt keine neuen Sperren hinzufügen.
San Jacinto

8
Ihr C ++ 11-Threading-Beispiel ist falsch . So ist die TBB ein, ist der Hinweis auf den Namen scoped Sperre .
Jonathan Wakely

3
Beides ist mir bewusst, @Jonathan. Sie schienen den Satz, den ich geschrieben habe, verpasst zu haben (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Die Verwendung der Sperrung mit Gültigkeitsbereich liegt beim Entwickler, je nachdem, welche Art von Anwendung er erstellt. Diese Antwort sollte das grundlegende Verständnis des Mutex-Konzepts ansprechen und nicht auf die Komplexität des Mutex-Konzepts eingehen. Ihre Kommentare und Links sind daher willkommen, liegen jedoch etwas außerhalb des Rahmens dieses Tutorials.
Nav

41

Während ein Mutex verwendet werden kann, um andere Probleme zu lösen, besteht der Hauptgrund dafür, dass sie sich gegenseitig ausschließen und dadurch eine sogenannte Race-Bedingung lösen. Wenn zwei (oder mehr) Threads oder Prozesse gleichzeitig versuchen, auf dieselbe Variable zuzugreifen, besteht die Möglichkeit einer Race-Bedingung. Betrachten Sie den folgenden Code

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

Die Interna dieser Funktion sehen so einfach aus. Es ist nur eine Aussage. Ein typisches Pseudo-Assembler-Äquivalent könnte jedoch sein:

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

Da alle äquivalenten Anweisungen in Assemblersprache erforderlich sind, um die Inkrementierungsoperation für i auszuführen, sagen wir, dass das Inkrementieren von i eine nicht atmosphärische Operation ist. Eine atomare Operation kann auf der Hardware mit der Garantie abgeschlossen werden, dass sie nicht unterbrochen wird, sobald die Befehlsausführung begonnen hat. Das Inkrementieren von i besteht aus einer Kette von 3 atomaren Anweisungen. In einem gleichzeitigen System, in dem mehrere Threads die Funktion aufrufen, treten Probleme auf, wenn ein Thread zur falschen Zeit liest oder schreibt. Stellen Sie sich vor, wir haben zwei Threads, die gleichzeitig ausgeführt werden, und einer ruft die Funktion unmittelbar nach dem anderen auf. Nehmen wir auch an, wir haben i auf 0 initialisiert. Nehmen wir außerdem an, dass wir viele Register haben und dass die beiden Threads völlig unterschiedliche Register verwenden, sodass es nicht zu Kollisionen kommt. Der tatsächliche Zeitpunkt dieser Ereignisse kann sein:

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

Was passiert ist, ist, dass wir zwei Threads haben, die i gleichzeitig inkrementieren. Unsere Funktion wird zweimal aufgerufen, aber das Ergebnis stimmt nicht mit dieser Tatsache überein. Es sieht so aus, als ob die Funktion nur einmal aufgerufen wurde. Dies liegt daran, dass die Atomizität auf Maschinenebene "gebrochen" ist, was bedeutet, dass sich Threads gegenseitig unterbrechen oder zu falschen Zeiten zusammenarbeiten können.

Wir brauchen einen Mechanismus, um dies zu lösen. Wir müssen den obigen Anweisungen eine Bestellung auferlegen. Ein üblicher Mechanismus besteht darin, alle Threads außer einem zu blockieren. Pthread Mutex verwendet diesen Mechanismus.

Jeder Thread, der einige Codezeilen ausführen muss, die möglicherweise gemeinsam genutzte Werte von anderen Threads gleichzeitig unsicher ändern (über das Telefon mit seiner Frau sprechen), sollte zuerst eine Sperre für einen Mutex erhalten. Auf diese Weise muss jeder Thread, der Zugriff auf die gemeinsam genutzten Daten benötigt, die Mutex-Sperre durchlaufen. Nur dann kann ein Thread den Code ausführen. Dieser Codeabschnitt wird als kritischer Abschnitt bezeichnet.

Sobald der Thread den kritischen Abschnitt ausgeführt hat, sollte er die Sperre für den Mutex aufheben, damit ein anderer Thread eine Sperre für den Mutex erhalten kann.

Das Konzept eines Mutex scheint etwas seltsam, wenn man Menschen betrachtet, die exklusiven Zugang zu realen, physischen Objekten suchen, aber wenn wir programmieren, müssen wir absichtlich sein. Gleichzeitige Threads und Prozesse haben nicht die soziale und kulturelle Erziehung, die wir haben, daher müssen wir sie zwingen, Daten gut auszutauschen.

Wie funktioniert ein Mutex technisch gesehen? Leidet es nicht unter den gleichen Rennbedingungen, die wir zuvor erwähnt haben? Ist pthread_mutex_lock () nicht etwas komplexer als ein einfaches Inkrementieren einer Variablen?

Technisch gesehen benötigen wir Hardware-Unterstützung, um uns zu helfen. Die Hardware-Designer geben uns Maschinenanweisungen, die mehr als eine Sache tun, aber garantiert atomar sind. Ein klassisches Beispiel für eine solche Anweisung ist das Test-and-Set (TAS). Wenn wir versuchen, eine Sperre für eine Ressource zu erlangen, prüfen wir möglicherweise mithilfe des TAS, ob ein Wert im Speicher 0 ist. Wenn dies der Fall ist, ist dies unser Signal dafür, dass die Ressource verwendet wird und wir nichts (oder genauer) tun Wir warten durch einen Mechanismus. Ein Pthreads-Mutex versetzt uns in eine spezielle Warteschlange im Betriebssystem und benachrichtigt uns, wenn die Ressource verfügbar wird. Bei dümmeren Systemen müssen wir möglicherweise eine enge Schleife durchführen und den Zustand immer wieder testen. . Wenn der Wert im Speicher nicht 0 ist, setzt der TAS den Speicherort auf einen anderen Wert als 0, ohne andere Anweisungen zu verwenden. Es' Es ist so, als würde man zwei Montageanweisungen zu einer kombinieren, um Atomizität zu erhalten. Daher kann das Testen und Ändern des Werts (falls eine Änderung angemessen ist) nicht unterbrochen werden, sobald er begonnen hat. Wir können Mutexe auf einer solchen Anweisung aufbauen.

Hinweis: Einige Abschnitte ähneln möglicherweise einer früheren Antwort. Ich nahm seine Einladung zur Bearbeitung an, er bevorzugte die ursprüngliche Art und Weise, also behalte ich das, was ich hatte, was mit ein wenig seiner Redewendung durchsetzt ist.


1
Vielen Dank, San. Ich habe auf Ihre Antwort verlinkt :) Eigentlich hatte ich beabsichtigt, dass Sie meine Antwort + Ihre Antwort nehmen und als separate Antwort veröffentlichen, um den Fluss aufrechtzuerhalten. Es macht mir nichts aus, wenn Sie einen Teil meiner Antwort wiederverwenden. Wir machen das sowieso nicht für uns.
Nav

13

Das beste Thread-Tutorial, das ich kenne, ist hier:

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

Ich finde es gut, dass es sich eher um die API als um eine bestimmte Implementierung handelt und einige nette einfache Beispiele enthält, die Ihnen helfen, die Synchronisation zu verstehen.


Ich bin damit einverstanden, dass es definitiv ein gutes Tutorial ist, aber es enthält viele Informationen auf einer einzigen Seite und die Programme sind lang. Die Frage, die ich gestellt habe, ist die Mutex-Version der Rede "Ich habe einen Traum", in der Neulinge einen einfachen Weg finden, um Mutexe kennenzulernen und zu verstehen, wie die nicht intuitive Syntax funktioniert (dies ist eine Erklärung, die in allen Tutorials fehlt). .
Nav

7

Ich bin kürzlich auf diesen Beitrag gestoßen und denke, dass er eine aktualisierte Lösung für den c ++ 11-Mutex der Standardbibliothek benötigt (nämlich std :: mutex).

Ich habe unten einen Code eingefügt (meine ersten Schritte mit einem Mutex - ich habe die Parallelität unter win32 mit HANDLE, SetEvent, WaitForMultipleObjects usw. gelernt).

Da es mein erster Versuch mit std :: mutex und Freunden ist, würde ich gerne Kommentare, Vorschläge und Verbesserungen sehen!

#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! Danke fürs Schreiben. Wie ich bereits erwähnt habe, bestand mein Zweck nur darin, das Konzept eines Mutex einfach zu erklären. Alle anderen Tutorials machten es mit den hinzugefügten Konzepten der Hersteller-Verbraucher- und Zustandsvariablen usw. sehr schwierig, was es mir sehr schwer machte zu verstehen, was in aller Welt vor sich ging.
Nav

4

Die Funktion pthread_mutex_lock()entweder erwirbt den Mutex für den anrufenden Thread oder blockiert den Thread , bis der Mutex erworben werden können. Der zugehörige pthread_mutex_unlock()gibt den Mutex frei.

Stellen Sie sich den Mutex als Warteschlange vor. Jeder Thread, der versucht, den Mutex abzurufen, wird am Ende der Warteschlange platziert. Wenn ein Thread den Mutex freigibt, wird der nächste Thread in der Warteschlange beendet und ausgeführt.

Ein kritischer Abschnitt bezieht sich auf einen Codebereich, in dem Nichtdeterminismus möglich ist. Dies liegt häufig daran, dass mehrere Threads versuchen, auf eine gemeinsam genutzte Variable zuzugreifen. Der kritische Abschnitt ist nicht sicher, bis eine Art Synchronisation vorhanden ist. Eine Mutex-Sperre ist eine Form der Synchronisation.


1
Ist garantiert, dass genau der nächste Versuchsthread eingegeben wird?
Arsen Mkrtchyan

1
@Arsen Keine Garantie. Es ist nur eine hilfreiche Analogie.
Chrisaycock

3

Sie sollten die Mutex-Variable überprüfen, bevor Sie den durch den Mutex geschützten Bereich verwenden. Ihr pthread_mutex_lock () könnte also (abhängig von der Implementierung) warten, bis mutex1 freigegeben wird, oder einen Wert zurückgeben, der angibt, dass die Sperre nicht erhalten werden konnte, wenn jemand anderes sie bereits gesperrt hat.

Mutex ist wirklich nur ein vereinfachtes Semaphor. Wenn Sie darüber lesen und verstehen, verstehen Sie Mutexe. Es gibt verschiedene Fragen zu Mutexen und Semaphoren in SO. Unterschied zwischen binärem Semaphor und Mutex , wann sollten wir Mutex verwenden und wann sollten wir Semaphor verwenden und so weiter. Das Toilettenbeispiel im ersten Link ist ungefähr so ​​gut wie man es sich vorstellen kann. Der Code überprüft lediglich, ob der Schlüssel verfügbar ist, und reserviert ihn gegebenenfalls. Beachten Sie, dass Sie nicht wirklich die Toilette selbst reservieren, sondern den Schlüssel.


1
pthread_mutex_lockkann nicht zurückkehren, wenn jemand anderes das Schloss hält. Es blockiert in diesem Fall und das ist der springende Punkt. pthread_mutex_trylockist die Funktion, die zurückgegeben wird, wenn die Sperre gehalten wird.
R .. GitHub STOP HELPING ICE

1
Ja, ich wusste zunächst nicht, um welche Implementierung es sich handelt.
Makis

3

Für diejenigen, die das Shortex-Mutex-Beispiel suchen:

#include <mutex>
using namespace std;

int main() {
    mutex m;

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

    return 0;
}

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.