Condition Variables



  • Tag zusammen,

    Ich möchte gerne, daß ein Thread, der Daten in einen Puffer schreibt, einem anderen Thread Bescheid sagt, daß neue Daten zu lesen da sind. Der Puffer selbst ist ein "Lock-freier" Ringbuffer, d.h. ich muß das Lesen und Schreiben eigentlich nicht per Mutex o.ä. absichern.

    Mein aktueller Code, der auch zu funktionieren scheint, sieht etwa so aus:

    boost::condition cond;
    
    // in den Puffer schreiben
    buffer.write(...);
    cond.notify_one();
    
    // aus dem Puffer lesen
    for (;;) {
        if (buffer.available()) {
            buffer.read(...);
            ...
        } else {
            boost::mutex mutex;
            boost::mutex::scoped_lock lock(mutex);
            cond.wait(lock);
        }
    }
    

    Eigentlich könnte man sich doch den Mutex hier komplett sparen, oder? Also, wenn boost::condition::wait() nicht einen gelockten Mutex als Parameter erwarten würde...
    Die Codebeispiele, die ich gefunden habe, benutzen alle einen Mutex, der sowohl zum Lesen, als auch zum Schreiben vorher gelockt wird. Aber welchen Sinn hat der Mutex in meinem Anwendungsfall?

    Ist das falsch was ich hier mache, oder gibt es eine schönere Lösung? Vielleicht stehe ich auch einfach nur ein bißchen auf dem Schlauch...



  • hola

    Der Puffer selbst ist ein "Lock-freier" Ringbuffer, d.h. ich muß das Lesen und Schreiben eigentlich nicht per Mutex o.ä. absichern.

    wie funktioniert so ein lock-freier ringbuffer ?

    unter windows koenntest du z.b. APCProc mit QueueUserAPC dafuer verwenden.
    ob es dafuer ein equivalent in boost gibt weiß ich aber nicht.

    Meep Meep



  • Meep Meep schrieb:

    wie funktioniert so ein lock-freier ringbuffer ?

    Kann ich dir jetzt so nicht erklären, hab den schließlich nicht selbst geschrieben ;). Aber wenn du's genauer wissen möchtest, der Code ist eigentlich ganz gut verständlich: ringbuffer.h, ringbuffer.c.
    Wie gesagt, der kommt ohne Mutexes aus, Einschränkung ist aber, daß es nur je einen Lese- und einen Schreibe-Thread geben darf. Und wenn der Puffer voll ist, kann halt nicht mehr geschrieben werden.

    Meep Meep schrieb:

    unter windows koenntest du z.b. APCProc mit QueueUserAPC dafuer verwenden.
    ob es dafuer ein equivalent in boost gibt weiß ich aber nicht.

    Wenn schon platformabhängig, dann bräuchte ich eine Lösung für POSIX. Wenn irgend möglich würde ich aber ganz gerne bei Boost.Thread bleiben. Mein Code funktioniert ja anscheinend. Ich wüßte nur halt gerne, ob es eine ganz große Dummheit ist, das so zu machen, oder ob das so ok ist...



  • der "lock free ring buffer" code ist ... zumindest fragwürdig. auf einen alpha oder powerpc wird der code z.b. nicht funktionieren. auch je nach compiler könnte es sein dass der code versagt.

    dein code mit mutex/condvar ist auch falsch.

    Aber welchen Sinn hat der Mutex in meinem Anwendungsfall?

    lies doch bitte die doku, hm?
    die mutex wird u.u. auch verwendet um die internas der condition variable zu schützen.
    d.h. du darfst eine condition variable auch immer nur mit einer mutex (!) verwenden. d.h. auch alle threads müssen für die selbe condition variable die selbe mutex verwenden. d.h. einfach so eine lokale mutex anzulegen ist schonmal grundfalsch.

    wieso gehst du eigentlich davon aus dass der scoped-lock parameter sinnlos ist? ich meine dachtest du dir den hat jmd. da nur so aus spass eingebaut?



  • hustbaer schrieb:

    der "lock free ring buffer" code ist ... zumindest fragwürdig. auf einen alpha oder powerpc wird der code z.b. nicht funktionieren. auch je nach compiler könnte es sein dass der code versagt.

    Mit welcher Begründung? Der Code wird seit Jahren in etlichen Projekten verwendet (ja, auch auf PowerPC), und bisher habe ich da nichts von Problemen gehört.

    dein code mit mutex/condvar ist auch falsch.

    Das habe ich mir gedacht.

    Aber, gehen wir jetzt einmal davon aus, daß der Ringbuffer so funktioniert wie er sollte. Und außerdem davon, daß der Schreibe-Thread nicht durch das Warten auf einen Mutex blockiert werden darf. Wie würde man das dann richtig machen?

    die mutex wird u.u. auch verwendet um die internas der condition variable zu schützen.
    d.h. du darfst eine condition variable auch immer nur mit einer mutex (!) verwenden. d.h. auch alle threads müssen für die selbe condition variable die selbe mutex verwenden. d.h. einfach so eine lokale mutex anzulegen ist schonmal grundfalsch.

    wieso gehst du eigentlich davon aus dass der scoped-lock parameter sinnlos ist? ich meine dachtest du dir den hat jmd. da nur so aus spass eingebaut?

    Natürlich hat der Parameter seinen Sinn. Das einzige was mir nicht ganz klar ist, ist warum ich den in diesem Fall brauche.

    Wäre mein Code denn in Ordnung, wenn ich den Mutex außerhalb der Funktion anlege? Also etwa so:

    boost::condition cond;
    boost::mutex mutex;
    
    // in den Puffer schreiben
    buffer.write(...);
    cond.notify_one();
    
    // aus dem Puffer lesen
    for (;;) {
        if (buffer.available()) {
            buffer.read(...);
            ...
        } else {
            boost::mutex::scoped_lock lock(mutex);
            cond.wait(lock);
        }
    }
    


  • Nope, du musst die Mutex auch für "notify" locken soweit ich weiss. Lies das bitte in der Doku nach. Auch "notify" muss an den internas der condition variable rumfummeln, daher ist normalerweise auch dort ein mutex lock fällig.
    Wenn die verwendete Library es nicht von dir verlangt dann macht sie intern selbst ein lock auf eine Mutex/Spinlock, und es könnte erst wieder blockieren.

    Mit welcher Begründung der Code fragwürdig ist? Weil z.B. keine expliziten Memory-Barriers vorkommen. D.h. es ist nicht garantiert in welcher Reihenfolge Änderungen sichtbar werden, ES SEI DENN der C Compiler welcher das übersetzt macht irgendwelche freiwilligen (=vom Standard nicht vorgeschriebenen) Garantien bezüglich volatile.
    Und wenn die Reihenfolge nicht garantiert ist, dann könnte es passieren dass eine Änderung an "write_ptr" sichtbar wird bevor die neu kopierten Daten sichtbar sind. D.h. es würde der Leser z.B. ein paar byte alte Daten lesen, "vermischt" mit den neuen.

    Ich verstehe aber nicht ganz warum da nix blockieren darf - man muss halt nur sicherstellen dass bloss sehr kurz blockiert wird.



  • hustbaer schrieb:

    Nope, du musst die Mutex auch für "notify" locken soweit ich weiss. Lies das bitte in der Doku nach.

    Die Boost.Thread Doku erwähnt bei notify_one() nichts dergleichen. Wenn der Mutex vorher gelockt sein müßte, wäre das ja hoffentlich als Precondition angegeben.

    Mit welcher Begründung der Code fragwürdig ist? Weil z.B. keine expliziten Memory-Barriers vorkommen. D.h. es ist nicht garantiert in welcher Reihenfolge Änderungen sichtbar werden, ES SEI DENN der C Compiler welcher das übersetzt macht irgendwelche freiwilligen (=vom Standard nicht vorgeschriebenen) Garantien bezüglich volatile.
    Und wenn die Reihenfolge nicht garantiert ist, dann könnte es passieren dass eine Änderung an "write_ptr" sichtbar wird bevor die neu kopierten Daten sichtbar sind. D.h. es würde der Leser z.B. ein paar byte alte Daten lesen, "vermischt" mit den neuen.

    Ok... Also bräuchte man die Memory-Barrier, damit nicht nur read_ptr und write_ptr, sondern auch die Änderungen am "gemallocten" Speicher garantiert richtig beim anderen Thread ankommen?

    Ich verstehe aber nicht ganz warum da nix blockieren darf - man muss halt nur sicherstellen dass bloss sehr kurz blockiert wird.

    Naja, was ist denn "sehr kurz"?

    Der Ringbuffer ist Teil von JACK, einem Echtzeit-Audio-Server. Wenn ein Prozess zu lange braucht, seine Audio-Daten zu verarbeiten (also in diesem Fall z.B. einen Ringbuffer zu füllen, der dann von einem Thread mit niedrigerer Priorität gelesen wird), dann gibt's beispielsweise Knackser in der Audio-Ausgabe.
    Beim einem Mutex gibt es doch keine Garantie, wie lange es dauert, bis der Mutex gelockt ist, richtig? D.h. wenn man sowas in dem Echtzeit-Thread macht, dann kann das zwar gut gehen, und wird es wohl in den meisten Fällen auch, es kann aber ggf. auch böse Folgen haben.



  • Aus der Boost.Thread Doku:

    Note that the same mutex is locked before the shared data is updated, but that the mutex does not have to be locked across the call to notify_one.

    Also hast du Recht, du musst die Mutex nicht locken für das Notify. Was IMO ein Designfehler ist, da es die Implementierung der Condition-Variable dazu zwingt intern ein eine eigene Mutex/SpinLock zu verwenden, was unnötig Zeit kostet.

    Ok... Also bräuchte man die Memory-Barrier, damit nicht nur read_ptr und write_ptr, sondern auch die Änderungen am "gemallocten" Speicher garantiert richtig beim anderen Thread ankommen?

    Genau.
    Auf Systemen wo es sowieso garantiert ist dass es auch ohne die Barriers geht zerfallen diese auch "zu nichts", also auch kein Performance Verlust in dem Fall. Im Linux Kernel sind AFAIK einige Barriers vordefiniert (als Makros bzw. Funktionen), die sollte man verwenden können.

    Beim einem Mutex gibt es doch keine Garantie, wie lange es dauert, bis der Mutex gelockt ist, richtig? D.h. wenn man sowas in dem Echtzeit-Thread macht, dann kann das zwar gut gehen, und wird es wohl in den meisten Fällen auch, es kann aber ggf. auch böse Folgen haben.

    Ich kenne die genauen Regeln nicht nach denen der Linux Scheduler arbeitet. Es könnte u.u. sogar einen Deadlock geben, nämlich wenn man eine SpinLock verwendet, und der Realtime Thread nie Rechenzeit abgibt solange er "runnable" ist. Und keine andere CPU frei ist (z.b. weil es nur eine gibt).

    ----

    Ganz allgemein aber: wenn nicht gelockt werden darf kannst du condition variablen IMO gleich wieder vergessen, denn die garantieren alle nicht dass nicht intern irgendwo irgendwas gelockt werden könnte. Es muss doch für genau diese Fälle Möglichkeiten im Linux Kernel geben, also wie man "normale" Threads aus einem Realtime Thread heraus aufwecken kann. Wie man das unter Windows macht könnte ich dir sagen, aber mit Linux kenne ich mich nicht wirklich aus was diese Dinge angeht.



  • hustbaer schrieb:

    Ganz allgemein aber: wenn nicht gelockt werden darf kannst du condition variablen IMO gleich wieder vergessen, denn die garantieren alle nicht dass nicht intern irgendwo irgendwas gelockt werden könnte. Es muss doch für genau diese Fälle Möglichkeiten im Linux Kernel geben, also wie man "normale" Threads aus einem Realtime Thread heraus aufwecken kann. Wie man das unter Windows macht könnte ich dir sagen, aber mit Linux kenne ich mich nicht wirklich aus was diese Dinge angeht.

    boost::condition::notify_one() macht auf einem POSIX-System ein einfaches pthread_cond_signal(), was im Zusammenhang mit JACK "ok" zu sein scheint (aber sicherlich auch irgendwo intern ein Mutex verwendet).

    Der wirklich 100%ig sichere Weg wäre dann anscheinend, vor dem notify_one() erstmal ein try_lock() zu machen. Wenn das try_lock() fehlschlägt muß man es eben irgendwie später nochmal zu versuchen...



  • Was man so alles falsch in Erinnerung hat 🙂
    pthread_cond_signal() verlangt anscheinend wirklich nicht dass etwas gelockt wird (hab jetzt nochmal nachgeguckt). Hätte geschworen dass das anders ist. Komisch.
    In dem Fall kann man vermutlich (hoffentlich) davon ausgehen dass es OK ist, auch mit realtime Threads.

    Der wirklich 100%ig sichere Weg wäre dann anscheinend, vor dem notify_one() erstmal ein try_lock() zu machen. Wenn das try_lock() fehlschlägt muß man es eben irgendwie später nochmal zu versuchen...

    Nö, denn intern wird mit ziemlicher Sicherheit ein anderes Teil gelockt als die Mutex die man mit der Condition-Variable zusammen verwendet. Wird eher ein interner Scheduler-Lock sein, oder ne private Mutex der Condition-Variable.

    Allerdings geht es hier eh um Locks die üblicherweise "sehr kurz" gehalten werden, sollte also IMO kein Problem geben.

    BTW: ich habe unter Windows Stream-Sounds implementiert - das haut auch mit ganz normalen Mutexen einwandfrei hin, WENN man eben aufpasst nirgends Funktionen aufzurufen die recht lange blockieren während man die entsprechenden Locks hält. Allerdings nicht mit "realtime" Threads sondern ganz normalen high-priority Threads (+1 oder +2).


Anmelden zum Antworten