condition_variable: was soll der lock?



  • Hallo zusammen,
    ich will einen Worker-Thread bei Bedarf pausieren und später wieder aufwecken.
    Dazu hab ich folgende Helferklasse geschrieben:

    class ThreadCondition
    {
    public:
      ThreadCondition(bool val = false) : mVal(val) {}
    
      void Set(bool val = true) {
        {
          boost::mutex::scoped_lock lock(mMutex);
          mVal = val;
        }
        mCondVar.notify_all();
      }
      void Wait() const {
        boost::unique_lock<boost::mutex> lock(mMutex);
        if (mVal) return;
        mCondVar.wait(lock);
      }
    
    private:
      mutable boost::mutex              mMutex;
      mutable boost::condition_variable mCondVar;
      bool                              mVal;
    };
    

    Der Workerthread ruft nun regelmäßig Wait auf, während der GUI thread mit Set den thrad aktiv oder schlafen legen kann.

    Abgesehen davon, dass ich es seltsam finde für diese standard Aufgabe eine extra Klasse schreiben zu müssen stört mich die zusätzliche mutex.
    Lässt sich das performanter machen?
    Ich würde statt mutex und bool z.B. gerne atomic<bool> verwenden.
    Was ich nicht verstehe ist, wieso condition_variable::wait einen externen lock benötigt.
    Was geht die condition_variable an, wie ich den Zugriff auf die eigentliche condition synchronisiere?



  • C14 schrieb:

    Was ich nicht verstehe ist, wieso condition_variable::wait einen externen lock benötigt.
    Was geht die condition_variable an, wie ich den Zugriff auf die eigentliche condition synchronisiere?

    Es interessiert sie gar nicht. Wie willst du den Mutex wieder freigeben, wenn der thread gerade im wait der Condition steckt?



  • Mal davon abgesehen, verwendest du wait in diesem Kontext wahrscheinlich falsch. Umfassendes Tutorial fuer C++11 gibts hier: http://www.youtube.com/playlist?list=PL1835A90FC78FF8BE . Aber um auf deine Fragen zu antworten (ohne Code angeschaut zu haben):

    C14 schrieb:

    Abgesehen davon, dass ich es seltsam finde für diese standard Aufgabe eine extra Klasse schreiben zu müssen

    Viele Wege fuehren nach Rom, du hast diesen gewaehlt. Gibt sicher andere.

    stört mich die zusätzliche mutex.

    Gemeinsame Variablen muessen geschuetzt werden.

    Lässt sich das performanter machen?

    Kommt drauf an.

    Ich würde statt mutex und bool z.B. gerne atomic<bool> verwenden.

    Warum nutzt du dann kein atomic_bool?

    Was ich nicht verstehe ist, wieso condition_variable::wait einen externen lock benötigt.

    Das sit das Konzept von condition variables.

    Was geht die condition_variable an, wie ich den Zugriff auf die eigentliche condition synchronisiere?

    Die Condition-Variable wird benoetigt, um zu signalisieren, dass sich die Bedingung moeglicherweise geaendert hat. Um zu ueberpruefen ob das tatsaechlich der Fall ist (spurious wakeup) muss die Bedingung geprueft werden. Die Bedingung nutzt gemeinsame Variablen, die geschuetzt werden muessen. Deswegen braucht wait den gleichen Mutex, um eben synchronen Zugriff auf die gemeinsame Variable bei der Pruefung zu gewaehrleisten.



  • Ohne Lock hättest Du ein Problem, das ohne race-condition zu realisieren. Du musst mVal auf jeden Fall schützen, da zwischen der Abfrage in Wait und dem mCondVar.wait der Wert von mVal sich sonst verändern könnte, was Du dann nicht mit bekommen würdest, da Du noch nicht angefangen hast, zu warten. Daher muss die Freigabe des Locks und der Beginn des Wartens atomar geschehen.

    if (mVal) return;
        // ohne Lock hier könnte ein anderer thread mVal verändern und die Condtion signalisieren
        mCondVar.wait(lock);
    


  • @manni66, knivel:
    Ich will symmetrisch zu Get() die mutex selber freigeben, bevor ich wait aufrufe:

    void Wait() const {
       {
          boost::mutex::scoped_lock lock(mMutex);
          if (mVal) return;
        }   
        mCondVar.wait();
      }
    

    Oder gar keine mutex verwenden sondern atomic operations.
    Die Synchronisation von Werten ausserhalb der condition_variable sollte sie nichts angehen.
    Was soll an meiner Verwendung von wait falsch sein?

    @tntnet:
    Was spielt das für eine Rolle ob sich der Wert von mVal vor dem Aufruf von wait ändert?
    condition_variable::wait lässt den thread doch unabhängig vom Wert von mVal schlafen.



  • C14 schrieb:

    Was soll an meiner Verwendung von wait falsch sein?

    Die Condition könnte sich bei deiner Variante ändern, bevor du wait() aufgerufen hast. Außerdem bräuchte die Condition Variable dann intern zusätzliche Synchronisation...

    Mit atomic Operations kannst du keinen Thread schlafen legen...



  • dot schrieb:

    Die Condition könnte sich bei deiner Variante ändern, bevor du wait() aufgerufen hast.

    Spielt wie gesagt keine Rolle, weil die mutex ja in wait sowieso wieder freigegeben wird und dann mVal modifiziert werden kann. Ob das vor oder in wait passiert ändert das Verhalten doch nicht.

    dot schrieb:

    Außerdem bräuchte die Condition Variable dann intern zusätzliche Synchronisation...

    Das braucht sie wahrscheinlich trotzdem, weil notify und notify_all ja von der mutex nichts wissen. In der boost Implementierung der condition_variable ist auf jeden Fall nochmal eine mutex enthalten.

    dot schrieb:

    Mit atomic Operations kannst du keinen Thread schlafen legen...

    👎 Davon hab ich auch nicht gesprochen. Es geht darum den bool und die mutex durch einen atomaren bool zu ersetzen. Nur welche mutex soll ich dann für wait locken?



  • @C14
    Deine Kritik an der Funktionsweise von Condition-Variablen lässt vermuten dass du das Problem das diese lösen nicht verstanden hast.

    Die Condition-Variable MUSS nämlich die Mutex freigeben, weil nur so das Freigeben der Mutex und das Eintragen in die "Warteliste" der Condition-Variable atomar erfolgen können.

    Wenn das nämlich nicht atomar ist, dann hat folgender Code ein Problem:

    void BoundedQueue::Push(Item item)
    {
        unique_lock lock(m_mutex);
        while (m_queue.size() >= m_limit)
            m_queueNotFullCondition.wait(lock);
    
        m_queue.push_back(item);
        m_queueNotEmptyCondition.notify_all();
    }
    
    Item BoundedQueue::Pop()
    {
        unique_lock lock(m_mutex);
        while (m_queue.empty())
            m_queueNotEmptyCondition.wait(lock);
    
        if (m_queue.size() == m_limit)
            m_queueNotFullCondition.signal_all();
    
        return m_queue.pop_front();
    }
    

    Wenn jetzt ein Thread Push() aufruft, und zwischen dem Freigeben der Mutex und dem Eintragen in die Warteliste ein anderer Thread Pop() macht...
    Naja, überleg dir mal selbst was dann passiert.
    Bzw. was passiert kann ich dir sagen: der Thread der Push() macht wird zu lange warten - u.U. ewig.
    Nu guck dir den Code durch, und versuch draufzukommen warum das passieren kann.

    Und wenn du das verstanden hast, dann sollte dir auch klar sein warum Condition-Variablen so arbeiten wie sie nunmal arbeiten.

    ps: Wenn du verstehen willst warum Dinge so sind wie sie sind, solltest du dir angewöhnen etwas über den Tellerrand zu gucken. Ich hab' mich hier absichtlich nicht auf dein Beispiel bezogen, weil dein Beispiel vollkommen irrelevant ist. Ja, es gibt Fälle wo sich das oben beschriebene Problem nicht ergeben würde, und daher das Unlock/Lock der Mutex innerhalb der wait() Funktion nicht nötig wäre. Diese Fälle sind aber nicht sonderlich interessant, und der minimale Performance-Hit der durch das unnötige Unlock/Lock entsteht sollte zu verschmerzen sein.
    Und weil Programmierer doof sind, ist es die weitaus bessere Entscheidung für diese Fälle keine "Mutex-lose" wait-Funktion anzubieten. Weil es Probleme durch falsch-Verwendung vermeidet.

    EDIT: Sinnfreie Behauptung gestrichen. Siehe Beitrag von tntnet und meine Antwort darauf.



  • Nehmen wir an, das Wait würde ohne Mutex funktionieren, also du verwendest Deine vorgeschlagene Implementierung:

    void Wait() const {
       {
          boost::mutex::scoped_lock lock(mMutex);
          if (mVal) return;
        }  
        mCondVar.wait();
      }
    

    Dann könnte folgendes passieren:

    • Thread 1: ruft Wait() auf und holt sich den Mutex
    • Thread 2: möchte mVal setzen und fängt daher an, auf den Mutex zu warten, blockiert aber zunächst, da Thread 1 ihn hat.
    • Thread 1: fragt mVal ab - mVal ist false
    • Thread 1: Destruktor vom scoped_lock gibt Mutex frei
    • Thread 2: Jetzt bekommt Thread 2 den Mutex, da er von Thread 1 frei gegeben wurde
    • Thread 2: er setzt mVal auf true
    • Thread 2: signalisiert die Condition
    • Thread 1: hat noch nicht angefangen zu warten und bekommt davon also gar nichts mit
    • Thread 1: fängt erst jetzt an zu warten, dass jemand die Condition signalisiert, obwohl mVal bereits true ist
    • Thread 1: wartet
    • Thread 1: wartet weiter
    • Thread 1: und wartet noch immer, da Thread 2 ja schon was anderes zu tun hat

    Daher ist es wichtig, dass der Mutex genau mit dem Anfang des Wartens frei gegeben wird. Und zwar atomar.



  • Ok, macht Sinn. Vielen Dank!



  • hustbaer schrieb:

    Ja, es gibt Fälle wo sich das oben beschriebene Problem nicht ergeben würde, und daher das Unlock/Lock der Mutex innerhalb der wait() Funktion nicht nötig wäre.

    @hustbaer: Nur so aus Neugier: könntest Du ein Beispiel nennen? Mir fällt spontan so kein Anwendungsfall ein, wo kein Mutex benötigt wird.



  • Wenn man nur auf ein Signal wartet, um weiter zu machen und keine Bedingung geprueft werden muss.



  • @tntnet
    Hihi, mir fällt gerade auf dass es mit Condition-Variablen überhaupt gar nicht so viele solche Fälle gibt.
    Da hab ich wohl etwas voreilig behauptet es gäbe solche Fälle 🙂

    Die Fälle die man sehr schön mit (Windows) manual-reset Events machen kann, gehen mit Condition-Variablen ja nicht, weil man da dann erst wieder zusätzlich ein Flag bräuchte. Also Cancel-Flag o.ä.

    Etwas aus der Praxis kann ich jetzt spontan nicht liefern.

    Wenn man die "Shutdown-Begingung" ignoriert kann man Fälle konstruieren. Aber spätestens beim Shutdown (Beenden des Threads) wirds dann etwas haarig.

    Also das einzige was ich mir vorstellen kann ist wenn man nicht wait() sondern timed_wait() verwendet. Läuft dann aber auf Polling bestimmter Daten (z.B. der Shutdown-Begingung) hinaus.



  • knivil schrieb:

    Wenn man nur auf ein Signal wartet, um weiter zu machen und keine Bedingung geprueft werden muss.

    Wobei das dann ein periodisches Signal sein muss.
    Oder man muss pollen.
    Oder man hat ein Problem 🙂



  • Multi-Threading ist überbewertet und schafft viel mehr Probleme als es löst. Wer Probleme will, der benutze condition_variable , mutex oder atomic s. Für alle anderen gibt es halbwegs vernünftige Abstraktionen wie boost::asio::io_service oder std::future .



  • Er.
    boost::asio::io_service oder std::future "sind" genau so Multitthreading.
    Sogar Shared-Memory Multitthreading.

    Und weil du Boost.Asio erwähnt hast...
    Boost.Asio schafft wesentlich mehr Probleme als es löst!



  • hustbaer schrieb:

    @tntnet
    Hihi, mir fällt gerade auf dass es mit Condition-Variablen überhaupt gar nicht so viele solche Fälle gibt.
    Da hab ich wohl etwas voreilig behauptet es gäbe solche Fälle 🙂

    Ich fand auch erst, dass es solche Fälle bestimmt gibt. Aber mir ist überraschenderweise auch nichts eingefallen. Daher meine Frage. Na dann wissen wir dann Bescheid 😉 .

    TyRoXx schrieb:

    Multi-Threading ist überbewertet und schafft viel mehr Probleme als es löst. Wer Probleme will, der benutze condition_variable , mutex oder atomic s. Für alle anderen gibt es halbwegs vernünftige Abstraktionen wie boost::asio::io_service oder std::future .

    Multi-Threading ist kein Ersatz für Async-I/O. Mit Multi-Threading kann ein Programm mehrere Cores nutzen. Multi-Threading sollte man dann einsetzen, wenn man etwas CPU-Intensives auf mehrere Cores verteilen kann. Und wenn es CPU-Intensiv ist, dann nützt Async-I/O so gar nichts.



  • tntnet schrieb:

    Multi-Threading ist kein Ersatz für Async-I/O.

    Hab ich irgendwie anders in Erinnerung.

    tntnet schrieb:

    Mit Multi-Threading kann ein Programm mehrere Cores nutzen. Multi-Threading sollte man dann einsetzen, wenn man etwas CPU-Intensives auf mehrere Cores verteilen kann. Und wenn es CPU-Intensiv ist, dann nützt Async-I/O so gar nichts.

    Also ist Async-I/O kein Ersatz für Multi-Threading. Das stimmt.



  • volkard schrieb:

    tntnet schrieb:

    Multi-Threading ist kein Ersatz für Async-I/O.

    Hab ich irgendwie anders in Erinnerung.

    tntnet schrieb:

    Mit Multi-Threading kann ein Programm mehrere Cores nutzen. Multi-Threading sollte man dann einsetzen, wenn man etwas CPU-Intensives auf mehrere Cores verteilen kann. Und wenn es CPU-Intensiv ist, dann nützt Async-I/O so gar nichts.

    Also ist Async-I/O kein Ersatz für Multi-Threading. Das stimmt.

    Stimmt eigentlich. So rum wir ein Schuh draus 👍 .

    Meine Aussage stützte sich auf die Aussage von TyRoXx, dass man statt Multi-Threading doch Async-I/O verwenden sollte. Ich habe da wohl zu kompliziert gedacht. Eigentlich meinte ich "Multi-Threading ist nicht nur dazu da, Async-I/O zu ersetzen" aber besser ist natürlich "Async-I/O ist kein Ersatz für Multi-Threading, es sein denn, Multi-Threading wurde verwendet, um zu verhindern, dass durch blockierendes I/O der komplette Prozess blockiert".



  • @tntnet
    TyRoXx hat nicht "Async I/O" geschreiben sondern " boost::asio::io_service ".
    Und boost::asio::io_service ist mal primär ein Task-Dispatcher.
    Den kann man auch verwenden um Tasks zu schedullen die CPU-bound sind.


Log in to reply