Forum: PC-Programmierung C++ condition_variable


von Threading_Guru (Gast)


Lesenswert?

Hallo,

ein Trigger-Thread soll ein paar Worker-Threads per condition_variable 
triggern sobald std::cin.get() per Tastatur ausgelöst wird.

Das Problem ist, dass wenn die Worker_Threads den ersten Trigger 
bekommen, dann in der while(1) Schleife beim nächsten condVar.wait nicht 
mehr blockiert wird.
Eigentlich sollte nach einem Trigger alles Worker_Threads bei 
condVar.wait wieder auf einen neuen Trigger warten. Das tun sie aber 
nicht und die Schleife rennt durch als gäbe es keine Blockierung.

Wenn ich das Prädikat []{return dataReady;} rauslasse ist das Problem 
sofort verschwunden. Allerdings soll man wegen der "spurious wakeups" 
nicht auf das Prädikat verzichten. Mit dem Prädikat habe ich allerdings 
das genannte Problem.

Hat jemand eine Idee wie man da weiter kommt?

1
std::mutex mutex_;
2
std::condition_variable condVar;
3
bool dataReady;
4
5
void Trigger_Thread(int nr)
6
{
7
    std::cout << nr << ". Trigger_Thread startet" << std::endl;
8
    while(1)
9
    {
10
        std::cin.get();
11
        {
12
            std::lock_guard<std::mutex> lck(mutex_);
13
            dataReady = true;
14
        }
15
        condVar.notify_all();
16
    }
17
}
18
19
void Worker_Thread(int nr)
20
{
21
    std::cout << nr << ". Worker_Thread startet" << std::endl;
22
    while(1)
23
    {
24
        {
25
            std::unique_lock<std::mutex> lck(mutex_);
26
            condVar.wait(lck,[]{return dataReady;});
27
        }
28
        // work work work
29
        std::cout << nr << " Work done." << std::endl;
30
    }
31
}
32
33
int main()
34
{
35
    std::thread thread1(Trigger_Thread,1);
36
37
    for(int i = 1; i <= 5; i++)
38
    {
39
        std::thread(Worker_Thread,i).detach();
40
    }
41
42
    thread1.join();
43
}

von Noch einer (Gast)


Lesenswert?

Normalerweise stellt man da eine wasserfeste Auftragsqueue in den 
Mittelpunkt. Das wait() und notify() teilt dann den Workern nur mit, sie 
sollen mal nachschauen, ob wieder was in der Queue ist.

So etwas selbst wasserfest bekommen? Besser nach multithreaded work 
queue googeln.

von Datareadx (Gast)


Lesenswert?

dataReadx sollte wieder auf false gesetzt werden.

von Threading_Guru (Gast)


Lesenswert?

Mit dem Prädikat würde im Prinzip alles wasserfest sein, nur sorgt es 
dafür, dass das Blockieren nicht mehr funktioniert.

Was ich völlig unlogisch finde ist, dass ohne das Prädikat bei wait() 
alles wunderbar blockiert aber mit dem Prädikat eben nicht.

Daraus lässt sich schließen, dass das Prädikat irgendwas am wait() 
manipuliert.

So wie ich es verstanden habe, sollte das Prädikat eine zusätzliche 
Bedingung sein um "spurious wakeups" zu unterbinden. Aber wieso wird 
durch diese optionale Prädikat das wait() völlig ausgehebelt?

Man braucht nur
1
condVar.wait(lck,[]{return dataReady;});
durch
1
condVar.wait(lck);
zu ersetzen und alles funktioniert wunderbar.

Das Dilemma:

- ohne Prädikat kann es zu "spurious wakeups" kommen.
- mit Prädikat funktioniert wait() nicht mehr.

von Threading_Guru (Gast)


Lesenswert?

Mit dataReady auf false stellen bin ich auch nicht weitergekommen, denn 
es ist die Frage wo auf false stellen?

Wenn man dataReady hinter wait() auf false stellt wird nur ein Thread 
getriggert und die anderen werden ausgesperrt. Es sollen aber diverse 
Threads getriggert werden.

Wenn man dataReady hinter notify_all() stellt wird überhaupt kein Thread 
mehr getriggert.

von Noch einer (Gast)


Lesenswert?

> dataReady sollte wieder auf false gesetzt werden.

Von welchem Worker Thread? Und wie soll es koordiniert werden, damit 
nach einem notify_all() alle Thread genau einmal laufen? Und was machst 
du wenn ein Worker noch läuft, während das nächste notify_all() gesendet 
wird...

>Mit dem Prädikat würde im Prinzip alles wasserfest sein

Mit Try&Error bekommst du es niemals wasserfest. Du kannst es nicht 
testen ob alle Aktionen, die atomar sein müssen, unter keinen Umständen 
unterbrochen werden.

von Wilhelm M. (wimalopaan)


Lesenswert?

So wie Du es umgesetzt hast, gibt es keine suprious wakeups ... da hast 
Du Du den Begriff falsch verstanden.

Ausserdem hast Du die Bedeutung des Prädikates falsch verstanden:

So steht es auch in der Doku:
1
while (!pred()) {
2
    wait(lock);
3
}

Du muss Dich entscheiden, ob einer / alle Threads aufgeweckt werden 
sollen. Und Du muss eine Wartebedingung formulieren für das Warte-Idiom.

: Bearbeitet durch User
von Threading_Guru (Gast)


Lesenswert?

Ja klar es gibt keine suprious wakeups weil ich mich ja im Code mit der 
weitere Bedingung gesichert habe. Nur sorgt es dafür, dass das 
Ereignisgesteuerte Triggern der Worker_Threads NICHT mehr funktioniert.
Wie gesagt das Prädikat hab ich nur reingemacht weil nach zB folgenden 
Quellen gesagt wird es gibt sonst "Zufälliges Aufwachen" spurious 
wakeup.

http://de.cppreference.com/w/cpp/thread/condition_variable/wait
http://www.grimm-jaud.de/index.php/blog/bedingungsvariablen

Ohne Prädikat geht ja alles wie gewollt.


Das Ziel ist ganz einfach:

Der Trigger_Thread soll ALLE Worker_Threads nach einem Ereignis 
triggern.

In diesem Fall ist das Ereignis std::cin.get();

In den Worker_Threads würde bei "// work work work" eine Aufgabe 
abgearbeitet werden. Danach startet die Schleife neu und soll bei wait() 
erneut auf ein neues Signal warten. Und genau das Warten funktioniert 
durch die Verwendung des Prädikates NICHT mehr.

von Wilhelm M. (wimalopaan)


Lesenswert?

Threading_Guru schrieb:
>
> http://de.cppreference.com/w/cpp/thread/condition_variable/wait
> http://www.grimm-jaud.de/index.php/blog/bedingungsvariablen

Die verstehst den Begriff des spurious wakeups einfach falsch.

Ein spurious wake-up ist dann wer Fall, wenn ein Thread aufwacht, aber 
sein Wartebdingung weiterhin erfüllt ist.

Es kommt nicht "einfach so" zu wake-ups ;-)

: Bearbeitet durch User
von Hans (Gast)


Lesenswert?

Wenn alle Worker-Threads jedes Ereignis genau einmal verarbeiten sollen, 
reicht ein Boolean als Prädikat nicht, denn das Boolean muss ja wieder 
zurückgesetzt werden, nachdem das Ereignis verarbeitet wurde.

Das mindeste wäre ein Counter, der vom Trigger_Thread auf 5 gesetzt wird 
und von jedem Worker_Thread dekrementiert. Wenn er auf 0 ist, haben alle 
Worker_Threads das Ereignis verarbeitet. Danach kann der Trigger_Thread 
das nächste Zeichen empfangen. Du bräuchtest dann noch eine zweite 
Condition-Variable, mit der die Worker_Threads dem Trigger_Thread 
signalisieren, dass sie fertig sind.

Die Lösung ist allerdings nur eingeschränkt parallel. Ein Ereignis wird 
zwar parallel von mehreren Threads bearbeitet, es kann aber nicht 
parallel schon das nächste empfangen und teilweise verarbeitet werden.

Wenn man mehr parallelisieren möchte, könnte man für jeden Worker_Thread 
eine eigene Queue anlegen, aus der er seine Arbeit holt. Der 
Trigger_Thread müsste dann jedes empfangene Ereignis in alle Queues 
legen. Dadurch kann der Trigger_Thread unabängig von den Worker_Threads 
neue Zeichen empfangen.

Am Ende kommt es drauf an, was Du überhaupt parallelisieren willst. Die 
einfachere Variante wäre, dass nicht alle 5 Threads das Ereignis 
bearbeiten müssen, sondern ein Thread alle Verarbeitungsschritte 
erledigt. Stattdessen könntest Du mehrere verschiedene Ereignisse 
gleichzeitig bearbeiten. Wenn beispielsweise "Hallo" empfangen wird, 
bearbeitet Thread 1 das "H", Thread 2 das "a", Thread 3 das "l" usw.

von Threading_Guru (Gast)


Lesenswert?

Ich hab jetzt eine funktionierende Lösung entwickelt ohne auf das 
Prädikat zu verzichten. Und zwar bekommt einfach jeder Workerthread 
seine eigene condition_variable, mutex und bool.
Der Trigger_Thread ruft dann per notify die Worker auf die in der 
Datenstrukturliste sind. Damit kann jeder Worker sein eigenes bool 
(dataReady) auf false stellen und das Prädikat ist erfüllt.

> Es kommt nicht "einfach so" zu wake-ups ;-)

Das ist interessant. Es kann tatsächlich sein dass ich es nicht 
verstanden habe aber alle Quellen sagen, dass wake-ups und auch lost 
wakeup tatsächlich "zufällig" oder besser gesagt nicht gewollt auftreten 
kommen können.

Zitat von hier:
https://www.video2brain.com/de/tutorial/sender-und-empfaenger
1
"So genannte spurious wakeup und lost wakeup. Was ist ein spurious wakeup? Da verwende ich eine Analogie. Es kann durchaus passieren, wenn ein Thread auf einen anderen Thread wartet, und er dann die Benachrichtigung erhält, dass der wartende Thread vermeintlich die Benachrichtigung bekommen hat, tatsächlich war aber das keine Benachrichtigung. Das ist wie, wenn Sie im Bett legen, und die Katze kratzt an der Tür, und Sie glauben, es war der Wecker. Das ist just Hintergrund-Geräusch. Das ist das Phänomen des spurious wakeup. Da gibt es noch das Phänomen des lost wakeup. Das lost wakeup ist das Phänomen, dass Ihr Wecker läutet und Sie sind noch gar nicht in dem Zustand, auf den Wecker zu hören. Das heißt zum Beispiel, Ihr Wecker läutet, bevor Sie ins Bett gehen. Das ist lost wakeup. Sie schlafen dann für ewig. Anders ausgedrückt, es werden nicht schöne Arten, den Lock zu produzieren. Gut, und weil es diese Phänomene gibt, schützt man sich mit dem Prädikat dagegen."

von Wilhelm M. (wimalopaan)


Lesenswert?

Threading_Guru schrieb:
> Ich hab jetzt eine funktionierende Lösung entwickelt ohne auf das
> Prädikat zu verzichten. Und zwar bekommt einfach jeder Workerthread
> seine eigene condition_variable, mutex und bool.

Sein eigenen mutex? Dann ist doch der Sinn des Mutex wohl ad absurdum 
geführt ...

>> Es kommt nicht "einfach so" zu wake-ups ;-)
>
> Das ist interessant. Es kann tatsächlich sein dass ich es nicht
> verstanden habe aber alle Quellen sagen, dass wake-ups und auch lost
> wakeup tatsächlich "zufällig" oder besser gesagt nicht gewollt auftreten
> kommen können.

An einem Digital-Rechner ist nichts zufällig ... ausser /dev/random ;-)

Gemeint ist mit "spurious wakeups" ein Performanceproblem, kein 
semantisches Problem (Manchmal hat das Problem auch den Namen 
"thundering herd").

Stell Dir vor, Du hast einen signalisierenden AT (Aktivitätsträger, 
Thread) und mindestens 2 wartende ATs. Die Wartebedingungen der 
wartenden ATs sind unterschiedlich, aber überlappend: also muss(!) man 
ein notifiy_all() statt eines notify_one() verwenden (eine CV mit 
unterschiedlichen, überlappende Wartebedingungen verlangen ein 
notify_all()) (überlappende Bedingungen sind Bedingungen, die 
gleichzeitig wahr / falsch werden können).

AT1
1
while (i < 0) {cv.wait();}

AT2
1
while (i < 5) {cv.wait();}

Wenn nun durch den signalisierenden AT bspw. i = 2 gesetzt wird, dann 
müssen ja beide wartenden ATs aufgeweckt werden(s.o., sonst kann es auch 
sein, dass der "falsche" AT aufgeweckt wird und nicht das passiert, was 
man semantisch will. Aus Sicht des Applikationsprogrammierers 
"erscheint" die Auswahl eines AT bei einem notify_one() zufällig, was 
aber bezogen auf das Gesamtsystem natürlich nicht ist), allerdings kann 
nur AT1 das Warteidiom überwinden, also weiterlaufen. AT2 legt sich 
sofort wieder schlafen. Für AT2 ist es ein "spurious wakeup".

: Bearbeitet durch User
von tictactoe (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Threading_Guru schrieb:
>> Ich hab jetzt eine funktionierende Lösung entwickelt ohne auf das
>> Prädikat zu verzichten. Und zwar bekommt einfach jeder Workerthread
>> seine eigene condition_variable, mutex und bool.
>
> Sein eigenen mutex? Dann ist doch der Sinn des Mutex wohl ad absurdum
> geführt ...

Nein, überhaupt nicht. Der Haupt-Thread muss ja auch noch mit den 
Worker-Threads kommunizieren, und das geht mit dem eigenen bool pro 
Worker-Thread, die jeder von einem Mutex geschützt werden müssen. Die 
gewählte Lösung macht sehr, sehr viel Sinn!

von Wilhelm M. (wimalopaan)


Lesenswert?

tictactoe schrieb:
> Wilhelm M. schrieb:
>> Threading_Guru schrieb:
>>> Ich hab jetzt eine funktionierende Lösung entwickelt ohne auf das
>>> Prädikat zu verzichten. Und zwar bekommt einfach jeder Workerthread
>>> seine eigene condition_variable, mutex und bool.
>>
>> Sein eigenen mutex? Dann ist doch der Sinn des Mutex wohl ad absurdum
>> geführt ...
>
> Nein, überhaupt nicht. Der Haupt-Thread muss ja auch noch mit den
> Worker-Threads kommunizieren,

das wissen wir mal wieder nicht so genau.

> und das geht mit dem eigenen bool pro
> Worker-Thread, die jeder von einem Mutex geschützt werden müssen.

Wie oben gesagt, braucht man die bool'sche flags wahrscheinlich gar 
nicht.

Vielleicht macht für sein Problem ein Counting-Semaphor mehr Sinn ... 
das hängt von seiner Definition des Begriffes "triggern" ab ;-)

: Bearbeitet durch User
von Hans (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Threading_Guru schrieb:
>> Ich hab jetzt eine funktionierende Lösung entwickelt ohne auf das
>> Prädikat zu verzichten. Und zwar bekommt einfach jeder Workerthread
>> seine eigene condition_variable, mutex und bool.
>
> Sein eigenen mutex? Dann ist doch der Sinn des Mutex wohl ad absurdum
> geführt ...

Nein, denn das Mutex schützt das boolsche Prädikat. Wenn jeder Thread 
sein eigenes Bool-Flag hat, kann auch jeder sein eigenes Mutex haben.

> An einem Digital-Rechner ist nichts zufällig ... ausser /dev/random ;-)
>
> Gemeint ist mit "spurious wakeups" ein Performanceproblem, kein
> semantisches Problem (Manchmal hat das Problem auch den Namen
> "thundering herd").
>
> [...] Aus Sicht des Applikationsprogrammierers
> "erscheint" die Auswahl eines AT bei einem notify_one() zufällig, was
> aber bezogen auf das Gesamtsystem natürlich nicht ist), allerdings kann
> nur AT1 das Warteidiom überwinden, also weiterlaufen. AT2 legt sich
> sofort wieder schlafen. Für AT2 ist es ein "spurious wakeup".

Nein, das hat damit überhaupt nichts zu tun. Die Condition-Variable weiß 
ja nichts über das Prädikat. Sie ist einfach ein Objekt, das bei Aufruf 
von wait() den Thread schlafen legt, bis ein anderer Thread notify() 
aufruft. Manchmal kehrt wait() aber auch "einfach so" zurück, ohne dass 
jemand notify() auf dem Objekt aufgerufen hat.

Dass man die Condition-Variable sinnvollerweise zusammen mit einem 
Prädikat in einer Schleife verwendet, ist die Konsequenz daraus. 
Semantisch ist das wie eine Busy-Waiting-Schleife auf das Prädikat. Der 
Unterschied ist nur, dass der Aufruf von wait() dazu führt, dass bis zum 
nächsten Aufwachen des Threads keine CPU-Zyklen verbraten werden. Ob das 
Prädikat beim Aufwachen erfüllt ist, ist nicht Teil der Semantik der 
Condition-Variable.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.