mehrere synchrone Anfragen von mehreren Threads in einem Thread sequentiell abarbeiten



  • Servus,

    ich leide gerade unter aktuer Hirnverknotung, da ich die letzten Tage mit Erlang rumgespielt habe, und da gibt es keine Threads und keine Locks.

    Das Problem ist, dass ich einen mulithreaded-teil einer Anwendung auf eine legacy-library zugreifen muss, wo nichts threadsafe ist.

    Die Multithreaded-Anwendung verwendet Qt, und somit QThread, QMutex usw...

    Das Problem ist im Grunde simpel: es gibt eine Funktion aus der c-library

    void request(int receiver, long message, void* data);
    

    diese Funktion darf einzig allein aus dem Hauptthread (wo main läuft) gecallt werden, sonst krachts.

    Jetzt kommt ein Teil der Anwendung hinzu, der in mehreren separaten threads läuft, aber auch der soll diese request-Funktion nutzen können.

    Daher wird aus allen anderen threads das dummy

    void safe_request(int receiver, long message, void* data);
    

    gecallt.

    Jetzt gibt es eine Funktion, die von main() gecallt wird (also aus dem Hauptthread), und in der will ich alle angesammelten safe_requests() in requests() abarbeiten.

    Dabei soll safe_request() einfach solange blockieren, bis die aus main aufgerufene Funktion den entsprechenden request() abgearbeitet hat und dann erst zurückkehren.

    Dafür müsste ich eine rekursive Mutex "verkehrt herum" verwenden, oder?
    Also die Mutex müsste standardmäßig gelockt sein, und jeder, der einen safe_request stellt, ebenfalls locken.
    Die Funktion aus dem main-thread müsste dann einen nach dem anderen Lock aufmachen und die requests() abarbeiten, und am Ende die mutex wieder locken (damit die anderen Threads nix machen können).

    Das klingt mir aber reichlich unelegant.
    QWaitCondition sieht mir irgendwie logischer aus, dass man eine waitcondition anlegt, und alle auf diese warten, und in main dann wakeAll() gemacht wird. Aber das geht nicht, weil ja dann alle threads gleichzeitig die funktion aufrufen würden. Das muss aber sequentiell passieren.
    Im Grund müsste man wakeOne() machen, solange warten, bis safe_request() zurückgekehrt ist, und dann den nächsten wakeOne(). Klingt auch bescheuert.

    Im Grunde muss ich also sämtliche Aufrufe von safe_request() serialisieren.

    Nachdem man einmal funktional programmiert hat, ist es ziemlich kontraintuitiv sich wieder über Locks gedanken machen zu müssen.
    Bin daher für Lösungen dankbar.

    Philipp



  • wenn du einen blockierenden mutex für die request()-funktion nimmst, müsste das doch schon reichen?

    lock(); // alle anderen warten
    request();
    unlock(); // der nächste kommt dran



  • DrGreenthumb schrieb:

    wenn du einen blockierenden mutex für die request()-funktion nimmst, müsste das doch schon reichen?

    lock(); // alle anderen warten
    request();
    unlock(); // der nächste kommt dran

    der request wird dann aber nicht im main-thread laufen, was ja nicht erlaubt ist.



  • Grob skizziert:

    void send_request(int param)
    {
        Request r(param);
    
        UniqueLock lock(q_queue.mutex);
        g_queue.PushBack(&r);
        while (!r.finished)
            r.finishedCondition.Wait(lock);
    }
    
    void process_requests()
    {
        UniqueLock lock(q_queue.mutex);
        while (!g_queue.empty)
        {
            Request* r = g_queue.PopFront();
            lock.unlock();
            r->Process(); // NOTE: Process() may not throw, otherwise we're supremely fucked because send_request() will block forever
            lock.lock();
            r->finished = true;
            r->finishedCondition.WakeAll();
        }
    }
    

    Alternativ kann man auch eine Condition-Variable für die ganze Queue verwenden, aber das führt zu mehr "spurious wakeups" von Threads die in send_request() warten dass "ihr" Request fertig wird. Dafür spart man sich das erzeugen + zerstören einer Condition-Variable pro Request (so eine Condition-Variable kann je nach Implementierung nicht ganz billig zu erzeugen sein).
    Je nachdem wie hoch die "Contention" der Queue ist kann das eine oder das andere mehr Sinn machen.

    Natürlich kann man die Condition-Variablen auch in der Queue poolen.
    Oder, noch eine andere Variante: man verpasst über Thread-Local-Storage jedem Thread seine eigene "send_request Condition-Variable" - send_request ist ja schliesslich nicht rekursiv.



  • krümelkacker schrieb:

    DrGreenthumb schrieb:

    wenn du einen blockierenden mutex für die request()-funktion nimmst, müsste das doch schon reichen?

    lock(); // alle anderen warten
    request();
    unlock(); // der nächste kommt dran

    der request wird dann aber nicht im main-thread laufen, was ja nicht erlaubt ist.

    stimmt ja. Aber dabei fällt mir jetzt wieder Qt ein: da gibts doch QApplication::postEvent.

    ich habe mir genau für sowas einen Wrapper darum gebaut: void postEvent (const boost::function<void()>&);

    in dem Fall wäre das dann mit postEvent (request); getan 💡

    nuja, das setzt natürlich das entsprechende event-system voraus...



  • DrGreenthumb schrieb:

    Aber dabei fällt mir jetzt wieder Qt ein: da gibts doch QApplication::postEvent.

    Eher QApplication::sendEvent, oder?
    postEvent wartet ja nicht bis das Ding fertig abgearbeitet wurde.



  • hustbaer schrieb:

    DrGreenthumb schrieb:

    Aber dabei fällt mir jetzt wieder Qt ein: da gibts doch QApplication::postEvent.

    Eher QApplication::sendEvent, oder?
    postEvent wartet ja nicht bis das Ding fertig abgearbeitet wurde.

    nur postEvent ist thread-safe. Und es wartet nicht, das fiel mir dann auch ein. Das ganze muss schon event-basiert laufen.

    Aber ich handhabe meine gesamte Thread-Kommunikation so und habe bisher nur gute Erfahrung damit.



  • DrGreenthumb schrieb:

    Aber ich handhabe meine gesamte Thread-Kommunikation so und habe bisher nur gute Erfahrung damit.

    Ja, wenn man ein Programm von vornherein darauf auslegt, kann man mit "post" gut auskommen (ob jetzt über Qt, Window-Messages oder was auch immer).
    Wenn man aber ein bestehendes Programm hat, das darauf nicht ausgelegt ist weil es überall "send" verwendet, ist es oft nicht so einfach.

    Dann lieber einen Wrapper stricken der aus "post" ein "send" macht - ist ja auch keine so schwere Aufgabe. Natürlich muss man dann aufpassen dass man sich nicht in einen Deadlock "sendet". Aber das muss man bei "send" ja immer.


Anmelden zum Antworten