2012-06-15 7 views
13

Ich lerne über C++ 11 Nebenläufigkeit, wo meine einzige vorherige Erfahrung mit Parallelen Primitiven in der Betriebssystemklasse vor sechs Jahren war, also sei sanft, wenn du kannst.C++ 11 std :: condition_variable: Können wir unsere Sperre direkt an den notifizierten Thread übergeben?

In C++ 11, können wir

std::mutex m; 
std::condition_variable cv; 
std::queue<int> q; 

void producer_thread() { 
    std::unique_lock<std::mutex> lock(m); 
    q.push(42); 
    cv.notify_one(); 
} 

void consumer_thread() { 
    std::unique_lock<std::mutex> lock(m); 
    while (q.empty()) { 
     cv.wait(lock); 
    } 
    q.pop(); 
} 

Dies funktioniert gut schreiben, aber ich bin von der Notwendigkeit beleidigt cv.wait in einer Schleife zu wickeln. Der Grund, warum wir die Schleife müssen, ist mir klar:

Consumer (inside wait())  Producer   Vulture 

release the lock 
sleep until notified 
           acquire the lock 
           I MADE YOU A COOKIE 
           notify Consumer 
           release the lock 
                acquire the lock 
                NOM NOM NOM 
                release the lock 
acquire the lock 
return from wait() 
HEY WHERE'S MY COOKIE        I EATED IT 

Nun, ich glaube, eine der coolen Dinge über unique_lock ist, dass wir es um passieren kann, nicht wahr? So wäre es wirklich elegant sein, wenn wir diese stattdessen tun könnten:

Consumer (inside wait())  Producer 

release the lock 
sleep until notified 
           acquire the lock 
           I MADE YOU A COOKIE 
           notify and yield(passing the lock) 
wake(receiving the lock) 
return from wait() 
YUM 
release the lock 

Jetzt gibt es keine Möglichkeit für den Geier Thread Schlag in, weil der Mutex der ganzen Weg I MADE YOU A COOKIE-YUM gesperrt bleibt. Plus, wenn notify() erfordert, dass Sie eine Sperre übergeben, ist das ein guter Weg, um sicherzustellen, dass Menschen den Mutex tatsächlich vor dem Aufruf (siehe Signalling a condition variable (pthreads)) sperren.

Ich bin mir ziemlich sicher, dass C++ 11 keine Standardimplementierung dieses Idioms hat. Was ist der historische Grund dafür (ist es nur, dass Pthreads es nicht getan hat? Und warum ist das)? Gibt es einen technischen Grund, dass ein abenteuerlustiger C++ - Coder dieses Idiom nicht in Standard C++ 11 implementieren konnte und es vielleicht my_better_condition_variable nannte?

Ich habe auch ein vages Gefühl, dass ich vielleicht Semaphoren neu erfinde, aber ich erinnere mich nicht genug von der Schule, um zu wissen, ob das genau ist oder nicht.

+3

Erm, wenn Sie das Cookie nicht essen einige Thread wollen Warum läufst du überhaupt? –

+2

Guter Punkt, der Ton meiner Frage war etwas voreingenommen gegen Geier. :) Siehe, Verbraucher hat einen schönen Traum. Er ist bereit, für Kekse und Milch geweckt zu werden, aber wenn er aufwacht und * keine * Kekse findet, wird er aufgebracht sein. Es ist also in Ordnung, wenn Producer Cookies an Vulture übermittelt, aber es ist nicht in Ordnung für Producer, den Consumer zu wecken, indem er ihn schüttelt und "COOKIE TIME!" Ruft. und dann "Oh, hoppla, Geier hat deinen Keks gegessen, während ich dich geschüttelt habe." Das ist nicht cool, Produzent. Das ist nicht cool. – Quuxplusone

+0

Das macht keinen Sinn. Sie können nicht mehrere nicht verwandte Threads erwarten, die nicht wissen, dass die anderen Threads, die auf dieselben freigegebenen Daten zugreifen, ordnungsgemäß funktionieren. Wie würde Vulture jemals mit Ihrer vorgeschlagenen Lösung Kekse essen? –

Antwort

10

Die ultimative Antwort ist, weil Pthreads es nicht getan haben. C++ ist eine Sprache, die Betriebssystemfunktionen kapselt. C++ ist kein Betriebssystem oder eine Plattform. Und so kapselt es die vorhandene Funktionalität von Betriebssystemen wie Linux, Unix und Windows.

Allerdings hat pthreads auch eine gute Begründung für dieses Verhalten. Von der Open Group Basis Spezifikationen:

Der Effekt ist, dass mehr als ein Thread von seinem Aufruf zu pthread_cond_wait zurückkehren kann() oder pthread_cond_timedwait() als Ergebnis einer call() pthread_cond_signal. Dieser Effekt wird "spurious wakeup" genannt.Beachten Sie, dass sich die Situation insofern selbst korrigiert, als die Anzahl der so erwachten Threads endlich ist; Zum Beispiel der nächste Thread zum Aufruf pthread_cond_wait() nach der Folge von Ereignissen über blockiert.

Während dieses Problem gelöst werden könnte, ist der Verlust an Effizienz für eine Randbedingung, die nur selten auftritt, ist nicht akzeptabel, vor allem gegeben, dass man das Prädikat mit einer Bedingung Variable ohnehin verbunden zu überprüfen hat. Das Korrigieren dieses Problems würde den Grad der Parallelität in diesem grundlegenden Baustein für alle höheren Synchronisierungsoperationen unnötig reduzieren.

Ein zusätzlicher Vorteil des Zulassens von unechten Wakeups ist, dass Anwendungen gezwungen werden, einen Prädikat-Test-Loop um die Condition Wait zu codieren. Dies macht auch die Anwendung tolerieren überflüssige Bedingung Broadcasts oder Signale auf der gleichen Bedingung Variable, die in einem anderen Teil der Anwendung codiert werden kann. Die resultierenden Anwendungen sind also robuster. Daher dokumentiert IEEE Std 1003.1-2001 explizit , dass unerwünschte Wakeups auftreten können.

Also im Grunde ist die Behauptung, dass Sie my_better_condition_variable auf einer pThreads Zustandsgröße (oder std::condition_variable) relativ leicht und ohne Leistungseinbußen zu bauen. Wenn wir jedoch my_better_condition_variable auf der Basisebene setzen, müssten diejenigen Clients, die die Funktionalität von my_better_condition_variable nicht benötigten, trotzdem dafür bezahlen.

Diese Philosophie, das schnellste, primitivste Design am unteren Ende des Stacks zu erstellen, mit der Absicht, dass bessere/langsamere Dinge darauf gebaut werden können, läuft durch die C++ - Bibliothek. Und wo die C++ - Bibliothek dieser Philosophie nicht folgt, sind Kunden oft (und zu Recht) irritiert.

+0

R. Martinho Fernandes und ich hatten eine nette Diskussion im Chat, also habe ich nicht viele unbeantwortete Fragen mehr. PThreads haben die Entscheidung getroffen, dass man einen Mutex nicht in einem Thread sperren und in einem anderen entsperren kann; Daher ist es unmöglich, in meinem Beispiel eine Sperre für einen PThreads-Mutex vom Producer zum Consumer zu übergeben (und 'std :: mutex' ist auf Pthreads modelliert). Das Erstellen von 'my_better_condition_variable' würde daher mehr Arbeit erfordern, als ich dachte, weil Sie * zuerst * my_unsafer_mutex erstellen müssen. Es wäre eine ganze Threads-Bibliothek, die parallel zur C++ 11-Bibliothek und damit nicht kompatibel ist. – Quuxplusone

+1

"Also im Grunde ist der Anspruch, dass man ... relativ einfach und ohne Leistungseinbußen bauen kann." "Und dann" Aber wenn wir setzen ... müsste man sowieso dafür bezahlen. * "Wenn es keine Leistungseinbußen gibt Was zahlen sie dann genau? – ildjarn

+1

@ildjarn: Wie in der Posix-Begründung erläutert, besteht die Notwendigkeit für die Schleife um die Wartezeit darin, dass die Wartezeit falsch (d. H. Ohne zu signalisieren) zurückkehren kann. Die Behauptung ist, dass es viel weniger teuer ist, eine Bedingungsvariable zu implementieren, die fälschlicherweise von einer Wartezeit zurückkehren kann, im Gegensatz zu einer, die dies nicht tut. Und die meisten Clients der Zustandsvariablen ignorieren ohnehin unerwünschte Weckvorgänge. Diese Kunden sollten also nicht für eine Zustandsvariable bezahlen müssen, die garantiert nicht fälschlicherweise zurückkehrt. –

3

Auch wenn Sie dies tun könnten, ermöglicht die C++ 11-Spezifikation cv.wait() entsperren entsperren (um Plattformen zu berücksichtigen, die dieses Verhalten haben). Selbst wenn es keine Vulture-Threads gibt (die Argumente, ob sie existieren sollten oder nicht), kann der Consumer-Thread nicht erwarten, dass dort ein Cookie wartet, und muss dies noch überprüfen.

5

Wenn Sie die Schleife nicht schreiben wollen, können Sie die overload verwenden, anstatt das Prädikat nimmt:

cv.wait(lock, [&q]{ return !q.is_empty(); }); 

Es in die Schleife äquivalent zu sein definiert ist, so funktioniert es ebenso wie die Original-Code .

+0

(Für die Aufzeichnung.) Mein Einwand gegen diese Lösung war, dass es immer noch eine Polling-Schleife, versteckt in der Implementierung von "warten". Wenn mein Code looping ist, würde ich eigentlich die explizite While-Schleife bevorzugen. – Quuxplusone

+2

@Quuxplusone: "Äquivalent zu" ist sehr weit entfernt von "implementiert in Bezug auf" in der C++ - Standard. "Äquivalent zu" erklärt die erwartete Semantik/das erwartete Ergebnis, aber das bedeutet nicht, dass es hier in irgendeiner Weise eine Literal-Abfrageschleife gibt. – ildjarn

0

Ich denke, das ist nicht sicher:

void producer_thread() { 
    std::unique_lock<std::mutex> lock(m); 
    q.push(42); 
    cv.notify_one(); 
} 

Sie sind nach wie vor die Sperre zu halten, wenn Sie einen anderen Thread wartet auf die Sperre informieren. So könnte es sein, dass der andere Thread sofort erwacht und versucht, die Sperre vor der Destruktor namens nachcv.notify_one() die Sperre zu lösen. Das bedeutet, dass der andere Thread zurückgeht, um irgendwann zu warten.

Also ich denke, das sollte codiert als:

void producer_thread() { 
    std::unique_lock<std::mutex> lock(m); 
    q.push(42); 
    lock.unlock(); 
    cv.notify_one(); 
} 

oder wenn Sie möchten entsperren nicht manuell als

void producer_thread() { 
    { 
     std::unique_lock<std::mutex> lock(m); 
     q.push(42); 
    } 
    cv.notify_one(); 
} 
+0

ist das wahr? - – dani