std::condition_variable - notify_one aufrufbar auch wenn lock noch nicht freigegeben



  • Hiho,

    ich habe mir eine (Hilfs-)Klasse) geschrieben, welche einen Funktor in einem extra Thread zyklisch aufruft und welche einige Callback-Punkte besitzt (cycle started, cylce finished, paused, resumed usw.)
    Synchronisiert wird das ganze per Mutex und einer Condition-Variable. Dazu habe ich nun eine Frage.

    Um z. B. den Thread zu pausieren, findet folgendes statt:

    void pause()
    {
    	std::unique_lock<std::mutex> lock(m_mut);
    
    	if (m_is_paused)
    		return;
    
    	m_do_work = false;
    	m_cond.notify_one();
    
    	//wait until the thread is paused to stay in a proper waiting state
    	while (!m_is_paused)
    		m_cond.wait(lock);
    }
    

    Der Mutex wird gesperrt, die Variable geprüft/gesetzt und dann notify_one aufgerufen, ohne das der Mutex freigegeben ist. In den Musterlösungen (z. B. hier) wird notify erst aufgerufen, nachdem der Lock freigegeben wurde. Meine Frage ist nun, geht meine Variante auch, oder kann es da unerwünschte Seiteneffekte geben?

    Der Grund, warum ich hier den Lock nicht freigebe, ist, dass ich im Anschluss noch warte, bis die Gegenseite signalisiert, dass sie wirklich pausiert hat. Dazu wartet das pause selber auf die condition-Variable, welches ja den lock implizit aufhebt beim Warten. Das ist dann quasi meine zweite Frage. Kann ich die Condition-variable auf diese Weise zur Synchronisation in beide Richtungen nutzen, ohne Seiteneffekte fürchten zu müssen?

    Hier noch der Code-Auszug von der Gegenseite:

    //worker loop
    std::unique_lock<std::mutex> lock(m_mut, std::defer_lock_t());
    for (;;)
    {
    	//check if the work should be paused or finished
    	lock.lock();
    	if (!m_do_work)
    	{
    		Callback_policy::paused();
    
    		//sets the state to paused
    		m_is_paused = true;
    		m_cond.notify_one();
    
    		//wait for wake signal
    		while (!m_do_work && !m_finished)
    			m_cond.wait(lock);
    
    		//set the state to not paused
    		m_is_paused = false;
    		m_cond.notify_one();
    
    		//check also here for finished to prevent a call of resumed
    		if (m_finished)
    			break;
    
    		Callback_policy::resumed();
    	}
    	
    . . .
    

    Ich kann auch gerne den Link zur gesamtem Header-Datei schicken, der Übersichtlichkeit halber habe ich hier nur die zwei Auszüge reingenommen.

    Prinzipiell scheint alles zu gehen, aber das Programm hat 3-4x am Tag Aussetzer mit der anliegenden Profibus-Kommunikation, wo wir noch nicht wissen warum. Und grade bei MT-Code passiert es gerne mal, dass beim Testen alles klappt und dann sporadisch doch Fehler auftreten, weil etwas nicht hinhaut. Daher meine Fragen.

    VG

    Chimaeros



  • OK, ich muss ergänzen, mit STD funktioniert das oben nicht. Ich hatte vorher alles mit boost realisiert, da ging alles. Vorhin habe ich alles auf std umgestellt und meine Frage gepostet und jetzt erst getestet und es geht seltsamer Weise nicht, ändert aber erstmal prinzipiell nichts an meinen Fragen drüber 🙂



  • @pellaeon sagte in std::condition_variable - notify_one aufrufbar auch wenn lock noch nicht freigegeben:

    ändert aber erstmal prinzipiell nichts an meinen Fragen drüber

    Wenn es nicht geht, ist das die Antwort.


  • Administrator

    Es scheint nicht zu gehen, zumindest wenn ich die englischsprachige C++ Referenz nehme:
    http://en.cppreference.com/w/cpp/thread/condition_variable/notify_one

    The notifying thread does not need to hold the lock on the same mutex as the one held by the waiting thread(s); in fact doing so is a pessimization, since the notified thread would immediately block again, waiting for the notifying thread to release the lock.

    Es ist also möglich, dass der benachrichtige Thread gar nie aus dem wait rauskommt. Und damit geht auch der erste Thread für immer schlafen und du hast ein Deadlock.



  • @dravere
    Ich würde den Satz nicht so interpretieren, dass es zu einem Deadlock kommt. Es scheint da eher um Aufwachen, Schlafen, wieder Aufwachen zu gehen. Weiter unten steht dann ja noch

    Notifying while under the lock may nevertheless be necessary when precise scheduling of events is required



  • @dravere sagte in std::condition_variable - notify_one aufrufbar auch wenn lock noch nicht freigegeben:

    does not need

    bedeutet aber doch nicht, dass es falsch ist?! Und in meinem Fall ist es doch sogar notwendig, weil durch den bestehenden Lock kann die pause-Methode in den Warte-Modus gehen, bevor der Arbeitsthread wiederrum sein notify aufruft. Würde er es zuerst aufrufenm, bevor das wait kommt, dann hätte ich doch erst einen Deadlock?!


  • Administrator

    @manni66 Bezüglich Deadlock meine ich nur in diesem Beispiel. Weil wenn er aufwacht und gleich wieder schlafen geht, dann wird er nie den ersten Thread aufwachen. Und beide Threads warten dann aufeinander.

    @Pellaeon Es ist nicht falsch im Sinne von undefiniert. Es hat definiertes Verhalten. Aber bei deinem Programm vermute ich, dass es zu einem Deadlock kommen könnte.

    Ich würde vorschlagen, dass du deine beiden Signale auseinander nimmst. Wieso auch die Condition Variable wiederverwenden? Das scheint eine seltsame "Optimierung" zu sein. Nimm eine condition variable und ein Mutex für das Signal zum Arbeitsthread und eine condition variable und ein Mutex für das Signal in die andere Richtung. Separation of Concerns.



  • Ok, das mit dem Fehler ziehe ich zuürck. Das Problem hatte nichts mit der Thematik hier zu tun, sondern damit, dass ich beim Umbenennen eine Methode vergessen hatte. Der Code läuft also auch mit std:thread usw. und nicht nur mit boost.

    Mit dem separation of concerns hast du natürlich recht. Mich würde es aber trotzdem interessieren, ob das so i. O. ist oder nicht. Soweit ich aber die Doku interpretiere, ist es prinzipiell erlaubt und ich sehe in meinem Ablauf keine Deadlock-Gefahr.


  • Administrator

    Habe mal probiert mit Pseudo-Sequenzdiagramm und Plantuml das darzustellen.

    Wenn der Worker notify_one aufruft, ist es möglich dass der erste Thread aufwacht und feststellt, dass die Mutex noch locked ist und gleich wieder einschläft. Und erst danach gibt der Worker in der wait Methode den Mutex frei. Aber dann sind beide Threads in der wait Methode gefangen.



  • @dravere sagte in std::condition_variable - notify_one aufrufbar auch wenn lock noch nicht freigegeben:

    Habe mal probiert mit Pseudo-Sequenzdiagramm und Plantuml das darzustellen.

    Wenn der Worker notify_one aufruft, ist es möglich dass der erste Thread aufwacht und feststellt, dass die Mutex noch locked ist und gleich wieder einschläft. Und erst danach gibt der Worker in der wait Methode den Mutex frei. Aber dann sind beide Threads in der wait Methode gefangen.

    AFAIK ist genau das nicht erlaubt. -Das schöne bei Condition-Variablen ist dass sowohl schlafenlegen+mutex-freigeben als auch aufwachen+mutex-locken atomare Operationen sind.-
    EDIT: OK, nein, aufwachen+mutex-locken ist nicht atomar, muss es aber auch nicht sein. Wichtig ist dass schlafenlegen+mutex-freigeben atomar ist. Bzw. einfach so wie in dem von @Dravere zitierten Abschnitt aus dem Standard beschrieben 🙂
    /EDIT
    Und wenn der Thread signalisiert wurde, dann wurde er signalisiert, und muss auch aufwachen. Egal ob die Mutex gerade gelockt ist oder nicht. Wenn sie gelockt ist, dann muss er halt warten bis sie frei wird, aber wenn sie frei wird dann muss er aufwachen.

    In deinem Beispiel, wenn ich es richtig verstehe, müsste also der "Notifier" beim finalen "wait" des "Worker" aus seinem eigenen "wait" wieder rauskommen.

    Wäre das nicht so, wären Condition-Variablen genau so frickelige Konstrukte wie Windows Events -- was sie aber nicht sind 🙂


  • Administrator

    @hustbaer Dann sagst du, dass die Erklärung von cppreference.com falsch ist? Ich gebe gerne zu, dass ich es nicht exakt weiss. Ich leite das nur, von der Aussage auf cppreference.com ab.


  • Administrator

    Hab das nun schon länger nicht mehr gemacht, aber wollte es nun wissen und habe den C++ Standard (Draft N4659) angeschaut. §33.5 Abschnitt 3

    3 The execution of notify_one and notify_all shall be atomic. The execution of wait, wait_for, and
    wait_until shall be performed in three atomic parts:

    1. the release of the mutex and entry into the waiting state;
    2. the unblocking of the wait; and
    3. the reacquisition of the lock.

    Und dann später in §33.5.3 Abschnitt 10ff bezüglich Definition von void wait(unique_lock<mutex>& lock);:

    10 Effects:
    (10.1) — Atomically calls lock.unlock() and blocks on *this.
    (10.2) — When unblocked, calls lock.lock() (possibly blocking on the lock), then returns.
    (10.3) — The function will unblock when signaled by a call to notify_one() or a call to notify_all(), or
    spuriously.

    Wodurch nach Standard die cppreference.com falsch ist. Wenn es nach Standard geht, sollte sich der Thread nicht wieder schlafen legen, wenn der Mutex gelockt ist, sondern einfach warten bis ein Lock erreicht werden kann und dann rausspringen.

    Somit müsste es also zu keinem Deadlock kommen, oder? Ich hasse Multithreading 😃



  • @dravere

    Dann sollte mein Code oben ja passen. Das notify_one in pause sorgt dafür, dass der Worker aus dem Schlafen, welches durch das wait verursacht wurde, aufwacht. Dann stellt er fest, dass der Lock noch existiert und wartet. Jetzt aber auf Grund eines normalen Mutex-Locks und nicht auf Grund eines wait-Aufrufs. Das Warten ist sogar essentiell in meinem Fall, weil die pause-Methode ihrerseits ein wait aufrufen will. Und das muss zwangsweise vor dem notify_one des workers erfolgen (ansonsten hätte ich eine Dead-Lock-Gefahr, wenn der Worker das notify_one vor dem wait ausführt).



  • @dravere Wenn man genauer drüber nachdenkt wird IMO klar dass es gar nicht anders sein kann. Wenn Benachrichtigungen "verlorengehen" könnten, bloss weil die Mutex gerade gelockt war, könnte man sich bei Condition-Variablen auf gar nix mehr verlassen. Sämtlicher Code wo mehr als zwei Threads beteiligt sind wäre anfällig auf Deadlocks.

    Beispielsweise ne einfache Bounded-Queue:

    void push(T const& t)
    {
        ScopedLock l(m_queueMutex);
        while (m_queue.size() >= MaxSize)
            m_queueShrinkCondition.wait();
        m_queue.push_back(t);
        m_queueGrowCondition.notify_all();
    }
    

    Ganz egal wie pop() nun implementiert ist, es besteht immer die Möglichkeit dass ein weiterer Thread gerade push() ausführt während m_queueShrinkCondition von pop() signaled wird, und daher m_queueMutex gelockt ist. Die Sache würde früher oder später zuverlässig deadlocken. Der Einzige Ausweg wäre immer nur mit Timeout zu warten - und das möchte man im Normalfall ja eher vermeiden.


  • Administrator

    @hustbaer Vielleicht. Wobei die Rede davon war, dass man den Mutex freigeben muss, bevor man notify_* aufruft. Nach dem was auf cppreference.com stand, ging ich davon aus, dass ein try_lock gemacht wird, wenn ein Thread durch ein notify_one aufwacht.

    @Pellaeon Genau. Zumindest so verstehe ich den Standard. Somit sollte der Code so funktionieren.



  • @dravere sagte in std::condition_variable - notify_one aufrufbar auch wenn lock noch nicht freigegeben:

    @hustbaer Vielleicht. Wobei die Rede davon war, dass man den Mutex freigeben muss, bevor man notify_* aufruft. Nach dem was auf cppreference.com stand, ging ich davon aus, dass ein try_lock gemacht wird, wenn ein Thread durch ein notify_one aufwacht.

    Ob man die Mutex vor dem notify_* freigeben muss oder nicht ändert nichts an dem Problem.

    Thread 1:

    void push(T const& t)
    {
        {
            ScopedLock l(m_queueMutex);
            while (m_queue.size() >= MaxSize)
                m_queueShrinkCondition.wait(); // <-------- schläft hier
            m_queue.push_back(t);
        }
        m_queueGrowCondition.notify_one();
    }
    

    Thread 2:

    void push(T const& t)
    {
        {
            ScopedLock l(m_queueMutex);
            while (m_queue.size() >= MaxSize)  // <-------- "läuft" gerade hier, hat die Mutex gelockt
                m_queueShrinkCondition.wait();
            m_queue.push_back(t);
        }
        m_queueGrowCondition.notify_one();
    }
    

    Thread 3:

    void pop()
    {
        ...
         m_queueShrinkCondition.notify_one(); // ruft signal_one auf ohne die Mutex gelockt zu haben
    }
    

    In dem Fall würde Thread 1 aufgeweckt werden, zu einem Zeitpunkt wo die Mutex gelockt ist -- obwohl der signalisierende Thread sie gar nicht gelockt hat. Würde Thread 1 hier nur ein try_lock machen, hätte man ein Problem.


  • Administrator

    @hustbaer Sehr gut erklärt, danke! Da hast du natürlich völlig recht.


Log in to reply