RAII und Threads



  • Hallo Leute,
    ich würde gerne etwas Diskutieren, was hustbar im folgenden Beitrag gesagt hat.
    @hustbaer sagte in Design Problem mit Dependency Injection:

    @pmqtt sagte in Design Problem mit Dependency Injection:

    Nämlich zuerst müssten zwingend die Broker zerstört werden. Da der Service in einem Thread blockiert und wenn das Objekt nicht zerstört wird, wartet der Thread bis in alle Ewigkeit auf das beenden der Verbindung.

    Verwendest du etwa das Schliessen eines Sockets um einen blockierenden accept/recv Aufruf o.Ä. zu beenden? Das ist nämlich ganz grob falsch nur so nebenbei.

    Erstmal der Rahmen:
    Wir habe eine Klasse Server in diesem existiert eine Klasse Messenger. Dieser verwaltet ein Socket, der geschlossen wird über disconnect oder über seinen Destruktor.

    class Server{
           Server(std::unique_ptr<Messenger> && m1):  t1(&Server::work,this),messenger(std::move(m1)){
           }
           ~Server(){
                    messenger->dicsonnect();
                    t1.join();
           }
    private:
           void work(){
                  try{
                        while(1){
                           std::string  msg = messenger->read(); //Blockiert bis Daten da sind oder eine Exception geworfen wird
                           ....
                        }
                  }catch(const DisconnectException & e){
                  }
           }
    
    private:
        std::thread t1;
        std::unique_ptr<Messenger> messenger;
    }
    

    Jetzt stellt sich für mich die Frage, wie sollte das Design aussehen, damit ich aus dem blockierenden Zustand raus komme. Denn irgendwann will ich den Thread ja zerstören.
    P.S dass es sich um einen Socket handelt ist jetzt einfach nur der Diskussion geschuldet. Es kann ja auch eine MessageQueue sein, die solange das read oder pop blockiert bis etwas da ist.



  • Ich kenne nur die Windows-Sockets, und die brechen bei einem Read ab, wenn die Verbindung abbricht oder geschlossen wird. Das musst du dann iwo in deinem messenger->read() behandeln. Ansonsten hast du ja keine Möglichkeit, den Thread sauber zu beenden.
    Die gleiche Möglichkeit musst du für alle anderen Cosumer natürlich auch zur Verfügung stellen.



  • @DocShoe so sehe ich das auch. Das Problem ist ich halte viel von @hustbaer und er wird eine Anmerkung, wie ist nämlich ganz grob falsch nicht einfach unmotiviert machen. Also für mich ist nur ein Design wie unten dargestellt möglich. Für dich anscheinend auch oder fällt dir etwas besseres ein ?

    Grüße,



  • Das ganz grob falsch bezieht sich doch nur auf die Art, wie du Sockets behandelst, nicht um das Design selbst. Und selbst das ist nicht klar, weil wir nicht wissen, wie du ein Socket schließt. Ich denke, hustbaers Kommentar bezieht sich auf eine Vermutung, die er hat.



  • Für mich stellt sich die Frage, ob man ein solches Szenario wie oben, auch mit RAII behandeln könnte. Das heißt, der Entwickler muss sie keine Gedanken darum machen, dass die Ressource geschlossen oder in diesem Fall released wird. So dass der join nicht an der fehlenden deblockade scheitern wird.

    Grüße



  • Es gibt nur einen mir bekannten Weg einen Socket zu schliessen, und das ist mit close (bzw. closesocket auf Windows). Etwas anderes ist es wenn es darum geht die Verbindung zu schliessen, dafür gibt es shutdown. Dabei bleibt das Socket-Handle/der Socket-File-Descriptor aber weiterhin gültig.

    Und das ist der Knackpunkt: Ein Programm das einen Socket mit close zu macht (und damit das Handle/den FD ungültig macht) hat ganz prinzipiell und grundlegend ein Problem. Weil du nie sicherstellen kannst dass das close wirklich passiert während recv/send wartet. D.h. es kann in so einem Programm immer vorkommen dass es close sagt und kurz danach das (nun bereits ungültige) Socket-Handle als Parameter für recv/send verwendet wird.

    Und noch schlimmer: Es kann sogar passieren dass das Handle zu dem Zeitpunkt bereits freigegeben und wieder "re-used" wurde. D.h. im schlimmsten Fall liest oder schreibst du dann mit einem Socket der ganz wem anderen gehört. Oder u.U. ein FD der gar kein Socket ist sondern z.B. ein normales File.


    Lösung: Als erstes würde ich mal versuchen shutdown zu verwenden. Ich bin mir nicht 100% sicher ob das funktioniert, hab's noch nie ausprobiert, aber könnte gehen.

    Ansonsten kenne ich nur reichlich hässliche Lösungen. Auf jeden Fall musst du auf non-blocking IO + select/poll/... o.ä. umschalten. Und beim select/poll/... musst du dann einen Dummy-Socket mitgeben den du von aussen "bereit" machen kannst, so dass select/poll/... zurückkommen. Auf POSIX Systemen kann man dazu schön pipe bzw. pipe2 verwenden. Auf Windows ist es etwas blöder. Und ich denke das ist ein Thema das nicht in einer kurzen Antwort hier im Forum ausreichend beschrieben werden kann.

    MMn. ist das auch ein grobes shortcoming der socket API, denn vor diesem Problem stehen Programmierer jeden Tag. Da es aber grundsätzlich lösbar ist, und es leider extrem lange braucht bis man sich darauf verlassen kann dass eine neue Funktion auch wirklich auf allen Systemen verfügbar ist die man unterstützen will... ist meine Hoffnung gering dass sich da in nächster Zeit was tun wird.



  • @hustbaer danke für die Antwort.
    Du hattest im anderen Thread davon gesprochen, dass man eine blockierendes read von einem Socket nicht mit close schliessen sollte. Soweit hatte ich dich auch verstanden. Für mich stellt sich tatsächlich die Frage, ob man ein wartenden Thread wie oben im Beispiel gezeigt, durch ein geschicktes Desgin aufwecken kann und den Entwickler davon entlastet, sich um das Aufwecken des Threads zu kümmern. Ähnlich wie beim unique_ptr muss ich mir keine Gedanken mehr machen, dass die Freigabe der Ressource angestoßen wird, möchte ich mir auch keine Gedanken machen, dass ein blockierender Thread wieder aufgeweckt wird, um diesen dann mit dem Parent Thread zu Synchronisieren. Also ich könnte es mir in etwa so vorstellen:

    
    
    class Thread{
    public:
            Thread(std::function<void()> & threadFun ,std::function<void()> & abortFunc) : t1(threadFunc),abort(abortFunc){ }
            ~Thread(){
                     abort();
                     t1.join();
            }
    private:
            std::thread t1;
            std::function<void()>  abort;
    };
    


  • @pmqtt
    Ja, klar kann man sowas machen.

    Was du hier gezeigt hast, also

    class Server{
           Server(std::unique_ptr<Messenger> && m1):  t1(&Server::work,this),messenger(std::move(m1)){
           }
           ~Server(){
                    messenger->dicsonnect();
                    t1.join();
           }
    private:
           void work(){
                  try{
                        while(1){
                           std::string  msg = messenger->read(); //Blockiert bis Daten da sind oder eine Exception geworfen wird
                           ....
                        }
                  }catch(const DisconnectException & e){
                  }
           }
    
    private:
        std::thread t1;
        std::unique_ptr<Messenger> messenger;
    }
    

    sieht für mich auch ganz vernünftig aus. Also mal abgesehen davon dass es falsch ist weil du den Thread erzeugst bevor messenger initialisiert ist (d.h. der Thread könnte auf messenger zugreifen bevor das Ding überhaupt initialisiert ist bzw. während es initialisiert wird), aber das lässt sich je einfach fixen.

    Ich würde das auch gar nicht weiter abstrahieren. Wozu, was gewinnst du damit? Das wichtige dabei ist dass der Benutzer von Server sich nicht darum kümmern muss etwas zu machen bevor er den Server zerstört. Und das hast du damit auch so erreicht.

    Das was ich als "ganz grob falsch" bezeichnet habe ist bloss der (vermutete) close/closesocket Aufruf in Messenger::disconnect.



  • @hustbaer ich habe die Klasse hier im Forum einfach geschrieben. Ohne viel Sinn und Verstand!
    Mir ist klar, dass deine Antwort sich speziell auf Sockets bezogen hat. Mich hat deine Anmerkung, generell zum Nachdenken darüber gebracht, ob das entblocken eines Threads nicht ähnlich wie das Speichermanagment mit unique_ptr oder shared_ptr geschehen sollte...
    Diese Frage beschäftigt mich.

    Im Ursprungs Thread von mir, verbirgt sich hinter dem Messenger kein Socket, sondern ein MQTT Client und dahinter verbirgt sich ein Socket.



  • @pmqtt sagte in RAII und Threads:

    @hustbaer
    Mich hat deine Anmerkung, generell zum Nachdenken darüber gebracht, ob das entblocken eines Threads nicht ähnlich wie das Speichermanagment mit unique_ptr oder shared_ptr geschehen sollte...
    Diese Frage beschäftigt mich.

    Naja... also grundsätzlich ist es natürlich so dass man sich bei Mustern die sich immer wieder ergeben überlegen sollte ob man sie sinnvoll abstrahieren kann.

    Die von dir skizzierte Thread Klasse könnte man schon als eine solche Abstraktion sehen. Die Frage ist nur ob es viel bringt. Also ob es oft genug in dieser einfachen Form vorkommt, und ob es mehr nutzt als schadet.
    Potentielle Nutzen wären:

    • Ausdrucksstärke durch einen zusätzlichen Namen und klare Semantik der Klasse.
      In deinem Beispiel heisst die Klasse nur "Thread", das würde ich auf jeden Fall ändern. Ich müsste jetzt länger nachdenken um einen guten Namen zu finden. Aber es sollte etwas sein was impliziert dass dies ein Thread ist den man weder joinen noch detachen will (kann), aber (mittels des Destruktors) jederzeit kontrolliert beenden kann.
    • Geringeres Risiko Fehler zu machen.
      Auch das ist IMO etwas vom Namen abhängig, denn je besser der Name der Klasse und der beiden Konstruktor-Parameter gewählt ist, desto geringer das Risiko dass jemand misversteht was das alles soll und es daher falsch verwendet.

    Im Ursprungs Thread von mir, verbirgt sich hinter dem Messenger kein Socket, sondern ein MQTT Client und dahinter verbirgt sich ein Socket.

    Naja das ändert ja nicht grundsätzlich etwas. Am Ende gibt es immer noch einen Socket, und dieser sollte nicht mit close geschlossen werden während er noch irgendwo in Verwendung sein könnte.

    Und auch daran wie ich den Rest entwerfen würde ändert sich nichts. Ich würde das auch dann so bauen dass erstmal der Server seinem Messenger sagt dass er jetzt Schluss machen soll, und dieser dann wiederrum seinem MQTT Client sagt dass er jetzt Schluss machen soll. Wegen Kapselung/Entkoppelung und so, es sollte den Server nicht interessieren wie genau der Messenger implementiert ist, dass hier ein MQTT Client im Spiel ist geht den Server eigentlich nix an.

    Also z.B.

    ...
        ~Server() {
            m_messenger->shutdown();
            m_thread.join();
        }
    ...
        Messenger::shutdown() {
            m_mqttClient->shutdown();
        }
    ...
        MqttClient::shutdown() {
            // black magic that makes blocking MqttClient calls on other threads return
        }
    

Anmelden zum Antworten