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



  • 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.


Anmelden zum Antworten