Mulithreaded listeners



  • Hallo zusammen,

    ich Frage mich gerade wie man folgende theoretische Problemstellung am einfachsten/elegantesten löst.
    Also ich habe eine Liste mit Listenern, die bei einem bestimmten Event aufgerufen werden sollen. Also generell denke ich als Datenstruktur zum Speichern der Listener an eine std::list oder einen std::vector. (Alternative Vorschläge sind willkommen, aber ich wüsste keine Struktur, die dieses Problem nicht hat)
    Jetzt möchte ich ein Fenster registrieren, das einmal auf dieses Event
    hören soll oder gar nicht, wenn der Benutzer (anderer Thread) es vorher schließt.
    Das generelle Problem liegt nun darin, dass die Iteratoren bei Einfüge- bzw. Löschoperationen ungültig werden. Ein Ändern von der Liste mit Listenern macht ja die Iteratoren ungültig - bei list zumindest, wenn der Iterator auf das gelöschte Element zeigt, was ja der Fall ist. Also muss ich die Liste irgendwie sperren während ich darüber iteriere oder eine Kopie machen (während der Erstellung muss ich natürlich auch sperren). Ich entscheide mich für eine Kopie, da ich ich ansonsten zur Vermeidung von deadlocks, das Entfernen des Listeners in einem neuen Thread machen müsste. Außerdem bräuchte man eine Kontrolle, die garantiert, dass eine remove-Operation Vorrang vor einer weiteren Iteration hat. Ich gehe mal vereinfacht davon aus, dass die Events nur von einem Thread gefeuert werden können, so dass kein paralleles Iterieren möglich ist.
    Problem bei der Erstellten Kopie ist natürlich, dass sobald die Iteration beginnt alle Elemente feststehen, die informiert werden müssen.
    Angenommen eine Iteration hat begonnen und der Benutzer schließt das Fenster und löscht damit das Objekt, das auf das Event hört. Dann zeigt einer der Listener in der Liste dann ins Nirvana. Ich gehe einfach einmal davon aus, dass die Liste der Listener Objekte des Typs std::function beinhaltet. Um jetzt zu verhindern, dass mein Listener ins Nirvana zeigt, verwende ich eine std::function, die einen weak_ptr beinhaltet, dessen Funktion nur aufgerufen wird, wenn der shared_ptr auf das Fenster-Objekt noch besteht.
    Ist das ein legitimer Ansatz?
    Also Kopie-Erstellung der Listener-Liste und weak_ptr damit das Zerstören des Objektes nicht verhindert wird und ein gelöschtes Objekt erkannt werden?

    Würdet ihr das ähnlich machen?
    Habt ihr einen völlig andere Weg?

    Gruß,
    XSpille



  • Hört sich wie Boost.Signals2 an.



  • Man kann vermeiden den Listener-Vektor zu kopieren.
    Geht relativ einfach über shared_ptr.

    Ist mit Code einfacher zu beschreiben als mit Worten:

    void foo::add_listener(listener const& new_listener)
    {
        scoped_lock l(m_mutex);
        if (m_listeners.unique())
            m_listeners->push_back(new_listener);
        else
        {
            shared_ptr<listener_vector> new_lv(new listener_vector(m_listeners->size());
    
            std::copy(m_listeners->begin(), m_listeners-end(), new_lv->begin());
            new_lv->back() = new_listener;
    
            m_listeners.swap(new_lv);
        }    
    }
    
    void foo::fire_event()
    {
        shared_ptr<listener_vector> lv;
    
        {
            scoped_lock l(m_mutex);
            lv = m_listeners;
        }
    
        // Über lv iterieren und Events abschicken
    }
    

    Das sollte in quasi allen vorstellbaren Fällen schneller sein als die Listener-Liste zu kopieren, oder zumindest nur unwesentlich langsamer.

    Wenn die Events sehr selten sind, aber dauernd neue Listener dazukommen oder alte entfernt werden, sind die Chancen sehr gering dass gerade ein Event gefeuert wird während die Listener-Liste modifiziert wird, d.h. m_listeners.unique() wird meistens true sein, d.h. der "langsame Pfad" wird fast nie genommen.
    Bleibt als potentieller Nachteil gegenüber der "Liste-Kopieren" Variante beim ändern der Listener-Liste nur noch das m_listeners.unique() und die zusätzliche Indirektion über den Zeiger.
    m_listeners.unique() hat sicher eine "atomic" Instruction drinnen, und ist damit nicht GANZ billig, aber SO schlimm ist es auch nicht - werden MAXIMAL 50% der Kosten des sowieso nötigen Mutex-Lock + -Unlock sein.
    Ich will jetzt nicht sagen dass das nie vorkommt -- es gibt einige Programme wo Events wesentlich öfter connected und disconnected werden als tatsächlich gefeuert.

    Wenn man so ein Programm hat, und es auf das letzte bisschen Performance ankommt, dann könnte es wirklich besser sein die Liste immer beim Senden von Events zu kopieren.

    Bzw. man kann den m_listeners.unique() Aufruf noch (auf Kosten der Event-Abschicken-Performance) wegoptimieren, indem man manuell einen "reader count" mitführt. Um den wieder anzupassen muss man nach dem Absenden der Events nochmal die Mutex locken -- dafür muss man beim Ändern der Listener-Liste keinen atomic Zugriff mehr machen sondern nur ganz normal eine Member-Variable auslesen. mMn. macht das auch halbwegs Sinn, da das Senden von Events sowieso schon relativ teuer ist -- man muss ja pro Connection den weak_ptr Locken und den so entstandenen shared_ptr wieder zerstören. Sobald mal ein paar Listener connected sind fallen die Kosten des zusätzlich nötigen Mutex-Lock nach dem Versenden dann nicht mehr so ins Gewicht.

    In allen anderen Fällen, also wenn Events häufiger gefeuert als connected/disconnected werden (oder ca. gleich oft), sollte die oben gezeigte Variante Vorteile bringen, da die Liste nur kopiert werden muss wenn sie verändert wird während gerade ein Event gefeuert wird. Was vermutlich nicht SO oft vorkommt.

    Die weak_ptr in den Funktoren brauchst du natürlich trotzdem.

    Würdet ihr das ähnlich machen?

    Ich habe das inetwa in der Form (mit der oben beschriebenen Optimierung) vor einiger Zeit in einem Projekt gemacht (wo Boost.Signals2 nicht verfügbar war).

    Wobei meine Listener-Liste ein Intrusive-Set aus ConnectionEntry Objekten war (sortiert nach Priorität). Die ConnectionEntry Objekte enthalten einen weak_ptr auf ein Connection Objekt. Und das Connection Objekt wiederrum enthält den eigentlichen Funktor (der dann oft wieder einen weak_ptr enthält). Der Client bekommt beim Connecten des Events dann einen shared_ptr<Connection> der die Connection am Leben erhält. Wenn er den zerstört/resettet wird (per Custom-Deleter) automatisch der ConnectionEntry Eintrag entfernt.

    Wobei sich speziell die Sache mit den Prioritäten im Nachhinein als Overkill herausgestellt hat. Ich hab's zwar an 1-2 Stellen verwendet, aber dort hätte man es vermutlich auch anders lösen können.


Anmelden zum Antworten