Multithreading und Class Inheritance



  • Hi, ich bin kein Mutithreading Experte und frage mich immer wieder wie man Multihreading und Class Inheritance bzw. Objektorientierte Programmierung am besten vereint. Häufig sehe ich den folgenden Ansatz den ich aber äußerst unbefriedigend empfinde:

    class ThreadedBase
    {
    public:
        virtual void AVirtualFunction1()
        {
            lock_guard<recursive_mutex> grd(_mutex);
            // Do something...
        }
    
    protected:
        recursive_mutex _mutex;
    };
    
    class ThreadedDerived : public ThreadedBase
    {
    public:
        virtual void AVirtualFunction1() override
        {
            lock_guard<recursive_mutex> grd(_mutex);
            // Do something else...
            ThreadedBase::AVirtualFunction1();
        }
    };
    

    Problem: Jeder der eine Klasse ableitet muss ggf. daran denke ebenso den Mutex zu setzen. Ist das so ein gängiges Vorgehen? Oder liegt der Fehler schon darin die Klasse aus unterschiedlichen Threadkontexten zu verwenden? In der abgeleiteten Klasse einen weiteren Mutex zu verwenden ist natürlich noch gefährlicher weil man dann u. U. schnell ein Deadlock gebaut hat wenn man eine gewisse Reihenfolge nicht einhält. Alternativ könnte man in den öffentlichen Funktionen z.B. nur boolsche Flags setzen um Aktionen zu aktivieren die dann später in einer "Poll" Funktion der Klasse ausgewertet und abgearbeitet werden. Wie macht ihr das?



  • @Enumerator sagte in Multithreading und Class Inheritance:

    Oder liegt der Fehler schon darin die Klasse aus unterschiedlichen Threadkontexten zu verwenden?

    Eher darin, abzuleiten. Solche Konstrukte sollten insgesamt möglichst selten vorkommen.
    Und wenn, dann kommt es drauf an. Dein Beispiel schreckt mich jetzt an sich auch nicht ab. Wenn man genau das braucht/will, dann ist es in Ordnung. Man sollte das nur nicht übertreiben und als gängiges Vorgehen betrachten.



  • @Enumerator sagte in Multithreading und Class Inheritance:

    AVirtualFunction1

    Wenn klar ist dass in AVirtualFunction1 immer gelockt werden muss, dann kann man das auch anders lösen.
    Nämlich indem man AVirtualFunction1 nicht-virtuell macht, dort lockt, und dann eine virtuelle Funktion aufruft:

    class ThreadedBase
    {
    public:
        void AFunction1()
        {
            lock_guard<recursive_mutex> grd(_mutex);
            AFunction1Impl();
        }
    
    protected:
        virtual void AFunction1Impl()
        {
            // Do something...
        }
    
        recursive_mutex _mutex;
    };
    
    class ThreadedDerived : public ThreadedBase
    {
    protected:
        virtual void AFunction1Impl() override
        {
            // Do something else...
            ThreadedBase::AFunction1Impl();
        }
    };
    

    Weniger Overhead da man nicht unnötig rekursiv lockt, und man kann nicht vergessen in abgeleiteten Klassen zu locken.



  • Mhhh, konkret geht es um ein UI Framework. Die Vererbungshierarchie ist ziemlich komplex. Da die UI bislang singlethreaded ist, CPUs aber immer mehr Threads haben bin ich halt am überlegen wie man davon profitieren könnte. Vor allem bei Aufrufen wie Arrange, Measure, Render usw. die den ganzen UI Baum entlanglaufen.

    Der Ansatz mit den Impl Klassen ist gut aber angesichts der Vielzahl der Funktionen schrecke ich davor noch etwas zurück.

    Es gibt nicht zufällig ein multithreaded OpenSource UI Framework wo man sich mal etwas inspirieren lassen könnte?



  • @Enumerator sagte in Multithreading und Class Inheritance:

    Mhhh, konkret geht es um ein UI Framework. Die Vererbungshierarchie ist ziemlich komplex. Da die UI bislang singlethreaded ist, CPUs aber immer mehr Threads haben bin ich halt am überlegen wie man davon profitieren könnte. Vor allem bei Aufrufen wie Arrange, Measure, Render usw. die den ganzen UI Baum entlanglaufen.

    Ich kenne dein Framework nicht, aber wenn das ein Objekt in einem einzigen zentralen UI-Baum für Layouting, Rendering und User-Code werden soll, dann halte ich das Locking hier für zu feingranular, um mehr Nutzen als Schaden zu bringen.

    Bist du ausserdem sicher, dass dein UI-System tatsächlich einen ganzen CPU-Kern auslastet? Die meisten UIs, die ich bisher gesehen habe schaffen das nichtmal ansatzweise, wenn man nur den UI-Teil betrachtet. Wenn's hakelt, dann ist das oft, weil zu viel Arbeit, die nichts mit UI zu tun hat in dem UI-Thread gemacht wird (als Extrembeispiel diese God-Buttons oder wie man die nennt, die kiloweise Arbeit und blockierendes IO machen, während das UI eingefroren ist). In so einem Fall könnte es helfen, z.B. UI und Code, der auf UI-Events reagiert in separaten Threads voneinander zu entkoppeln. Kommunikation z.B. über eine synchronisierte Message-Queue. Der UI-Thread könnte dann immer noch intern Single Threaded sein.

    Das ist aber nur ins Blaue geraten. Wenn du genau weisst, wo es hakt und ein bisschen das Design des Framework beschreibst, können wir vielleicht ein paar Tips geben, wie man das sinnvoll lösen könnte.



  • @Finnegan sagte in Multithreading und Class Inheritance:

    Ich kenne dein Framework nicht, aber wenn das ein Objekt in einem einzigen zentralen UI-Baum für Layouting, Rendering und User-Code werden soll, dann halte ich das Locking hier für zu feingranular, um mehr Nutzen als Schaden zu bringen.

    Ja das fürchte ich auch. Entweder müsste man alles komplett entkoppeln oder mit Lockguards zukleistern.

    @Finnegan sagte in Multithreading und Class Inheritance:

    Bist du ausserdem sicher, dass dein UI-System tatsächlich einen ganzen CPU-Kern auslastet?

    Nein, in 99% der Zeit passiert im Grunde nichts, wenn nicht z. B. irgendwelche Animationen laufen. Aber ein Szenario wo ich im Moment noch etwas Performance Probleme habe ist z. B. wenn das Fenster resized wird. Meine Idee war dass dann ein Control mit Child Controls anstatt direkt "Arrange" mit der neuen Größe aufzurufen dies für jedes Child als "Task" z. B. in eine TaskQueue ablegen könnte welche dann von einem ThreadPool abgearbeitet wird. Allerdings stellt sich die Frage ob der ganze Task-Overhead dann nicht wieder alles zunichte macht. Andererseits ist da auch noch einiges Optimierungspotenzial. Z.B. sollen die Menuitems später in einem eigenen (Child-)Window gerendert werden was dann die Arrange Kette durchbrechen würde. Und alleine ein umfangreiches Menü sind schnell je nach Style hunderte einzel Objekte.

    Wird schon seinen Grund haben das Microsoft in C#/WPF auch nur einen UI Thread hat. Evtl. macht es wirklich mehr Sinn dann mit "Wokerthreads" z.B. für Animationen zu arbeiten anstatt die ganze UI multithreaded zu gestalten.

    @Finnegan sagte in Multithreading und Class Inheritance:

    In so einem Fall könnte es helfen, z.B. UI und Code, der auf UI-Events reagiert in separaten Threads voneinander zu entkoppeln. Kommunikation z.B. über eine synchronisierte Message-Queue.

    Das hört sich interessant an. Evtl. kannst du nochmal ein kurzes Pseudocode Beispiel geben wie du das mit der synchronisierten MessageQueue meinst?



  • @Enumerator sagte in Multithreading und Class Inheritance:

    Nein, in 99% der Zeit passiert im Grunde nichts, wenn nicht z. B. irgendwelche Animationen laufen. Aber ein Szenario wo ich im Moment noch etwas Performance Probleme habe ist z. B. wenn das Fenster resized wird. Meine Idee war dass dann ein Control mit Child Controls anstatt direkt "Arrange" mit der neuen Größe aufzurufen dies für jedes Child als "Task" z. B. in eine TaskQueue ablegen könnte welche dann von einem ThreadPool abgearbeitet wird.

    Nur damit ich das richtig verstehe: Meine Interpretation dieser knappen Bescheibung ist, dass Kind-UI-Elemente Positionen und Größen relativ zu ihrem Eltern-Element haben. Die Arrange-Operation berechnet dann rekursiv die aktualisierten absoluten Koordinaten und speichert sie ebenfalls im UI-Baum. Ist das so korrekt?

    Bevor ich versuchen würde, das Problem mit mehreren Threads zu erschlagen, würde ich allderings sicherstellen wollen, dass mein Problem nicht irgendwo anders liegt, und mir folgende Fragen stellen:

    Wo hakt es denn da genau beim Resize? Kann einen ein Profiler da auf die richtige Spur bringen, oder ist "alles irgendwie langsam"?

    Wieviele Elemente müssen da angefasst werden? Muss jedes Element nur einmal durchlaufen werden oder hast du da irgendwelchen Code drin, der die Komplextät mit der Anzahl der Elemente rasant anwachsen lässt? Können bessere Algorithmen oder Datenstrukturen vielleicht helfen?

    Machst du vielleicht irgendwo mehr Arbeit als notwendig - z.B. braucht es nur einen rekursiven Arrange-Aufruf, wenn sich tatsächlich ein relevanter Parameter eines Eltern-Elemenst verändert hat. Auch sollte man drauf achten, dass man keine "kreuz- und quer-Rekursion" drin hat, wenn z.B. eine Größenänderung eines Kindelements wieder Einfluss auf die Größe des Elternelements haben kann.

    Allerdings stellt sich die Frage ob der ganze Task-Overhead dann nicht wieder alles zunichte macht. Andererseits ist da auch noch einiges Optimierungspotenzial. Z.B. sollen die Menuitems später in einem eigenen (Child-)Window gerendert werden was dann die Arrange Kette durchbrechen würde. Und alleine ein umfangreiches Menü sind schnell je nach Style hunderte einzel Objekte.
    Wird schon seinen Grund haben das Microsoft in C#/WPF auch nur einen UI Thread hat. Evtl. macht es wirklich mehr Sinn dann mit "Wokerthreads" z.B. für Animationen zu arbeiten anstatt die ganze UI multithreaded zu gestalten.

    Für simple UIs ist ein zentraler Baum sicher völlig in Ordnung. Worte wie "Animation" und "Rendering" lassen mich jedoch vermuten, dass du da etwas mehr mit vorhast. Damit kann man schon an Grenzen stossen - besonders wenn da tatsächlich User-, Layouting- und Rendering-Threads mit vielleicht >= 60fps durch diese eine Datenstruktur durchballern sollen.

    Eventuell macht es Sinn, nicht alle Eigenschaften des UI in einer einzigen Datenstruktur zu speichern, sondern mehrere Repräsentationen anzulegen, die auf den jeweiligen Anwendungsfall angepasst sind.

    So könnte es z.B. eine Baumstruktur für die user-seitige API geben und flache Array-Datenstrukturen für "Arrange"/Layoutberechnungen und für das letztendliche Rendering. Manchmal kann es nämlich tatsächlich performancetechnisch Sinn machen, mit zusätzlichem Aufwand Daten zu kopieren und sogar redundant vorzuhalten, wenn man sie damit in eine Datenstruktur bringt, die günstiger für einen Algorithmus oder die Arbeitsweise moderner CPUs ist (Cache und Branch Prediction als Stichworte).

    Solche separaten Datenstrukturen haben dann auch noch den Vorteil, dass man die Arbeit darauf leichter auf mehrere Threads aufsplitten kann, die dann mehr oder weniger ungestört auf ihren eigenen Datenstrukturen arbeiten können (mit nur wenigen Synchronisationspunkten).

    In diesem Kontext ist vielleicht auch ein Vortrag von der Cppcon 2018 über DatA-Oriented Design für dich interessant, vor allem weil es hier sehr konkret um UI geht. Hier auf dem "fancyness"-Niveau einer Browser-Engine - in diesem Fall für UIs innerhalb von Spielen:

    https://www.youtube.com/watch?v=yy8jQgmhbAU

    Der Vortrag geht nicht bis ins letzte Implementierungs-Detail, aber vielleicht dann man sich da trotzdem ein paar Ideen abholen.

    @Finnegan sagte in Multithreading und Class Inheritance:

    In so einem Fall könnte es helfen, z.B. UI und Code, der auf UI-Events reagiert in separaten Threads voneinander zu entkoppeln. Kommunikation z.B. über eine synchronisierte Message-Queue.

    Das hört sich interessant an. Evtl. kannst du nochmal ein kurzes Pseudocode Beispiel geben wie du das mit der synchronisierten MessageQueue meinst?

    Nichts furchtbar schlaues. Im einfachsten Fall reicht ein simpler std::queue der über einen einzigen Mutex synchronisiert wird. Die Idee ist, dass User-Code, der auf ein UI-Ereignis wie z.B. einen Knopfdruck reagiert, nicht im UI-Thread ausgeführt wird und diesen blockiert, während der User-Code z.B. gerade ein paar GiB Daten kopiert.

    Man hat in UI-Code ja öfter solche oder ähnliche Muster hier:

    
    // User-Thread
    user_code(Button button)
    {
       // viel zu tun!
       ...
    }
    ...
    button.registerEventHandler(BUTTON_PRESS, user_code);
    
    // UI-Thread (oder ebenfalls User-Thread, wenn Single-Threaded)
    button::on_press()
    {
        foreach (handler : button_press_handlers)
            handler(this); // ruft user_code() auf und hat viel zu tun -> UI Thread blockiert.
    }
    

    Die Idee ist nun, den User Code nicht im UI-Thread aufzurufen, sondern wieder im User-Thread, so dass der UI-Thread unbehelligt weiterarbeiten kann und seine "Schwuppdizität" nicht einbüsst (sehr grober Pseudocode, ohne Mutex-Synchronisation, etc):

    // UI-Thread
    button::on_press()
    {
        for (handler : button_press_handlers)
            message_queue.push_back(ButtonPressEvent(this, handler));
    }
    
    // User-Thread:
    process_events()
    {
        while (!message_queue.empty())
        {
             event = message_queue.pop_front();
             event.handler(event.object);
        }
    }
    ...
    button.registerEventHandler(BUTTON_PRESS, user_code);
    ...
    process_events();
    

Log in to reply