wake-up-neo.com

Wann wird eine Bedingungsvariable benötigt, ist ein Mutex nicht ausreichend?

Ich bin sicher, Mutex ist nicht genug, deshalb gibt es das Konzept der Bedingungsvariablen. aber es schlägt mich und ich bin nicht in der Lage, mich mit einem konkreten Szenario zu überzeugen, wenn eine Bedingungsvariable wesentlich ist.

nterschiede zwischen bedingten Variablen, Mutexen und Sperren Die akzeptierte Antwort der Frage besagt, dass eine Bedingungsvariable a ist

schloss mit einem "signalisierungs" mechanismus. Es wird verwendet, wenn Threads auf die Verfügbarkeit einer Ressource warten müssen. Ein Thread kann auf einen Lebenslauf "warten" und dann kann der Ressourcenproduzent die Variable "signalisieren". In diesem Fall werden die Threads, die auf den Lebenslauf warten, benachrichtigt und können die Ausführung fortsetzen

Wenn ich verwirrt bin, kann ein Thread auch auf einen Mutex warten, und wenn er signalisiert wird, bedeutet dies einfach, dass die Variable jetzt verfügbar ist. Warum sollte ich eine Bedingungsvariable benötigen?

P.S .: Außerdem ist ein Mutex erforderlich, um die Bedingungsvariable zu schützen, wenn meine Sicht dahingehend beeinträchtigt wird, den Zweck der Bedingungsvariablen nicht zu erkennen.

45
legends2k

Obwohl Sie sie so verwenden können, wie Sie es beschreiben, wurden Mutexe nicht für die Verwendung als Benachrichtigungs-/Synchronisationsmechanismus entwickelt. Sie sollen einen sich gegenseitig ausschließenden Zugriff auf eine gemeinsam genutzte Ressource ermöglichen. Mutexe zu verwenden, um einen Zustand zu signalisieren, ist umständlich, und ich nehme an, dies würde ungefähr so ​​aussehen (wobei Thread1 von Thread2 signalisiert wird):

Thread1:

while(1) {
    lock(mutex); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex); // Tells Thread2 we are done
}

Thread2:

while(1) {
    ... // do the work that precedes notification
    unlock(mutex); // unblocks Thread1
    lock(mutex); // lock the mutex so Thread1 will block again
}

Es gibt verschiedene Probleme damit:

  1. Thread2 kann die "Arbeit, die der Benachrichtigung vorausgeht" erst fortsetzen, wenn Thread1 die "Arbeit nach der Benachrichtigung" beendet hat. Bei diesem Entwurf ist Thread2 nicht einmal erforderlich. Verschieben Sie also "Arbeit, die vorangeht" und "Arbeit nach Benachrichtigung" in denselben Thread, da jeweils nur einer ausgeführt werden kann.
  2. Wenn Thread2 Thread1 nicht unterbinden kann, wird Thread1 den Mutex sofort wieder sperren, wenn er die while (1) -Schleife wiederholt, und Thread1 führt die "Arbeit nach Benachrichtigung" aus, obwohl es keine Benachrichtigung gab. Das bedeutet, dass Sie irgendwie garantieren müssen, dass Thread2 den Mutex sperrt, bevor Thread1 dies tut. Wie machst du das? Erzwingen Sie ein Zeitplanereignis möglicherweise, indem Sie in den Energiesparmodus wechseln oder auf andere betriebssystemspezifische Weise, aber es wird nicht garantiert, dass dies funktioniert. Dies hängt vom Timing, Ihrem Betriebssystem und dem Zeitplanalgorithmus ab.

Diese beiden Probleme sind nicht nur geringfügig, sie sind auch große Designfehler und latente Fehler. Der Ursprung dieser beiden Probleme ist die Anforderung, dass ein Mutex innerhalb desselben Threads gesperrt und entsperrt ist. Wie vermeiden Sie die oben genannten Probleme? Verwenden Sie Bedingungsvariablen!

Übrigens, wenn Ihre Synchronisationsanforderungen wirklich einfach sind, können Sie ein einfaches altes Semaphor verwenden, das die zusätzliche Komplexität von Bedingungsvariablen vermeidet.

28
slowjelj

Mutex ist für den exklusiven Zugriff auf gemeinsam genutzte Ressourcen vorgesehen, während die bedingte Variable darauf wartet, dass eine Bedingung erfüllt ist. Die Leute denken vielleicht, dass sie bedingte Variablen ohne die Unterstützung des Kernels implementieren können. Eine gebräuchliche Lösung, die man sich vorstellen könnte, ist das "Flag + Mutex" wie folgt:

lock(mutex)

while (!flag) {
    sleep(100);
}

unlock(mutex)

do_something_on_flag_set();

aber es wird niemals funktionieren, da Sie den Mutex während des Wartens niemals freigeben. Niemand sonst kann das Flag threadsicher setzen. Aus diesem Grund benötigen wir eine bedingte Variable. Wenn Sie auf eine Bedingungsvariable warten, wird der zugehörige Mutex nicht von Ihrem Thread gehalten, bis er signalisiert wird.

7
Dagang

Ich habe auch darüber nachgedacht und die wichtigste Information, die mir überall gefehlt hat, war, dass Mutex zu der Zeit nur einen Thread besitzen (und ändern) kann. Wenn Sie also einen Produzenten und mehrere Konsumenten haben, müsste der Produzent auf Mutex warten, um zu produzieren. Mit Kond. variabel kann er jederzeit produzieren.

5
MBI

Das bedingte Paar var und das Mutex-Paar können durch ein binäres Paar aus Semaphor und Mutex ersetzt werden. Die Reihenfolge der Operationen eines Consumer-Threads bei Verwendung des bedingten var + mutex ist:

  1. Sperren Sie den Mutex

  2. Warten Sie auf die bedingte var

  3. Prozess

  4. Schalte den Mutex frei

Die Reihenfolge der Operationen im Producer-Thread ist

  1. Sperren Sie den Mutex

  2. Signalisieren Sie die bedingte Var

  3. Schalte den Mutex frei

Die entsprechende Consumer-Thread-Reihenfolge bei Verwendung des Sema + Mutex-Paares lautet

  1. Warte auf das binäre Sema

  2. Sperren Sie den Mutex

  3. Überprüfen Sie den erwarteten Zustand

  4. Wenn die Bedingung erfüllt ist, verarbeiten Sie.

  5. Schalte den Mutex frei

  6. Wenn die Bedingungsprüfung in Schritt 3 falsch war, kehren Sie zu Schritt 1 zurück.

Die Reihenfolge für den Produzententhread lautet:

  1. Sperren Sie den Mutex

  2. Poste das binäre Sema

  3. Schalte den Mutex frei

Wie Sie sehen können, wird die bedingungslose Verarbeitung in Schritt 3 bei Verwendung der bedingten Variable durch die bedingte Verarbeitung in Schritt 3 und Schritt 4 bei Verwendung des binären Semas ersetzt.

Der Grund ist, dass bei Verwendung von sema + mutex in einer Racebedingung ein anderer Consumer-Thread zwischen Schritt 1 und 2 schleichen und die Bedingung verarbeiten/aufheben kann. Dies wird nicht passieren, wenn bedingte var verwendet wird. Bei Verwendung der bedingten Variable ist die Bedingung nach Schritt 2 garantiert erfüllt.

Das binäre Semaphor kann durch das reguläre Zählsemaphor ersetzt werden. Dies kann dazu führen, dass die Schleife von Schritt 6 bis Schritt 1 noch einige Male wiederholt wird.

3
Frank M

Sie benötigen Bedingungsvariablen, die mit einem Mutex verwendet werden (jede Bedingungsvariable gehört zu einem Mutex), um sich ändernde Zustände (Bedingungen) von einem Thread zu einem anderen zu signalisieren. Die Idee ist, dass ein Thread warten kann, bis eine Bedingung erfüllt ist. Solche Bedingungen sind programmspezifisch (d. H. "Warteschlange ist leer", "Matrix ist groß", "eine Ressource ist fast erschöpft", "ein Rechenschritt ist beendet" usw.). Ein Mutex kann mehrere verwandte Bedingungsvariablen haben. Und Sie benötigen Bedingungsvariablen, da solche Bedingungen möglicherweise nicht immer so einfach ausgedrückt werden wie "ein Mutex ist gesperrt" (Sie müssen also Änderungen an Bedingungen an andere Threads senden).

Lesen Sie einige gute Tutorials zu Posix-Threads, z. dieses Tutorial oder das oder das eins. Besser noch, lesen Sie ein gutes pthread-Buch. Siehe diese Frage .

Lesen Sie auch Advanced Unix Programming und Advanced Linux Programming

P.S. Parallelität und Fäden sind schwer zu fassende Begriffe. Nehmen Sie sich Zeit zum Lesen und Experimentieren und lesen Sie erneut.

Ich denke, es ist die Umsetzung definiert.
Ob der Mutex ausreicht oder nicht, hängt davon ab, ob Sie den Mutex als Mechanismus für kritische Abschnitte oder für etwas anderes betrachten.

Wie in http://en.cppreference.com/w/cpp/thread/mutex/unlock erwähnt,

Der Mutex muss vom aktuellen Ausführungsthread gesperrt sein, sonst ist das Verhalten undefiniert.

dies bedeutet, dass ein Thread nur einen Mutex entsperren konnte, der in C++ für sich selbst gesperrt war.
In anderen Programmiersprachen können Sie möglicherweise einen Mutex zwischen Prozessen gemeinsam nutzen.

Die Unterscheidung der beiden Konzepte kann sich also nur auf Leistungsaspekte beziehen. Eine komplexe Identifizierung der Eigentümer oder die gemeinsame Nutzung zwischen Prozessen sind für einfache Anwendungen nicht geeignet.


Beispielsweise können Sie den Fall von @ slowjelj mit einem zusätzlichen Mutex korrigieren (möglicherweise handelt es sich um eine falsche Korrektur):

Thread1:

lock(mutex0);
while(1) {
    lock(mutex0); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex1); // Tells Thread2 we are done
}

Thread2:

while(1) {
    lock(mutex1); // lock the mutex so Thread1 will block again
    ... // do the work that precedes notification
    unlock(mutex0); // unblocks Thread1
}

Ihr Programm wird sich jedoch darüber beschweren, dass Sie eine vom Compiler hinterlassene Behauptung ausgelöst haben (z. B. "Entsperren von nicht besessenem Mutex" in Visual Studio 2015).

0
Mr. Ree