Threads aus dem Konstruktor starten und Instanzen in Vector speichern



  • Hallo zusammen,

    ich habe mich heute relativ lange in folgender Situation verrannt:
    Ich habe eine Klasse geschrieben, deren Konstruktor unter anderem einen neuen Thread startet um ein paar asynchrone Initialisierungen vorzunehmen.

    Von dieser Klasse habe ich in einer Schleife ein paar Instanzen erstellt und diese einem Vector mit push_back() übergeben. Bei diesem push_back() werden die Instanzen allerdings kopiert und am Ende des Schleifenkopfes wird das ursprüngliche Objekt gelöscht, weil es nur lokal war und ich die Instanz nicht mit new erstellt hatte.

    Zur Verdeutlichung das Ganze kurz im Code:

    #include <thread>
    #include <chrono>
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    class MyClass
    {
    public:
      MyClass()
      {
        callingMethod(); 
      }
      void callingMethod()
      {
        thread myThread = thread(&MyClass::methodForMyThread, this);
        myThread.detach();
      }
    
    private:
      void methodForMyThread()
      {
        this_thread::sleep_for(chrono::milliseconds(100));
        cout<<this<<endl;            // Bis wir hier angekommen sind, wurde this
                                     // schon freigegeben und wieder überschrieben.
                                     // Deshalb bekommen wir zweimal die gleiche
                                     // Adresse auf der Kommandozeile zu sehen.
      }
    };
    
    int main()
    {
      vector<MyClass> myVec;
      for( int i = 0; i<2; i++ )
      {
        MyClass c = MyClass();       // Die Instanz wird nur lokal angelegt
        myVec.push_back(c);          // Die Instanz wird in den Vektor kopiert
      }                              // Das Objekt c wird freigegeben, obwohl der
                                     // Extra-Thread noch nicht fertig gelaufen is.
    
      this_thread::sleep_for(chrono::milliseconds(1000)); // alle zum Ende kommen lassen
      cout<<"Ende"<<endl;
      return 0;
    }
    

    Meine Frage ist nun: Wie verhindert man sowas "ordentlich"? Niemals im Konstruktor neue Threads starten? Oder ist es überhaupt Käse, innerhalb einer Methode einen neuen Thread zu starten? Wie würde man es sonst machen? Oder alle Threads im Destruktor joinen?

    Also die konkrete Anwendung wäre: Ich möchte im Konstruktor (und auch später) von außerhalb Daten abrufen, und diese mit Daten aus meinem Objekt abgleichen. Ich möchte aber nicht jedes Mal das ganze Programm lahmlegen, bis eine Antwort kommt, sondern stattdessen die Anfrage abschicken, einen inneren Zustand auf "nicht synchron" setzen und wenn die Antwort kommt, den Zustand entsprechend wieder auf "synchron" setzen.
    Und in meinem jugendlichen Leichtsinn dachte ich mir, mache ich halt einen Extra-Thread, der kann dann warten bis die Antwort kommt. Mit ein bisschen Schönheitskorrektur funktioniert das auch (jetzt wo ich weiß, was der Fehler war), aber vielleicht ist es ja trotzdem keine gute Idee.

    Gibt es eigentlich auch andere Klassen als meine, die nach dem Konstruktoraufruf nicht mehr kopiert werden dürfen? Und wenn ja, wie geht man damit um?

    Danke im Voraus für eure Antworten!

    Viele Grüße
    David



  • Move-Constructor und dann in den Vector herein-moven oder aber mit emplace_back gleich "in place" erstellen.



  • tkausl schrieb:

    Move-Constructor und dann in den Vector herein-moven oder aber mit emplace_back gleich "in place" erstellen.

    Dann aber vorher ein entprechendes reserve() , sonst passiert das gleiche.



  • Caligulaminus schrieb:

    tkausl schrieb:

    Move-Constructor und dann in den Vector herein-moven oder aber mit emplace_back gleich "in place" erstellen.

    Dann aber vorher ein entprechendes reserve() , sonst passiert das gleiche.

    Oh stimmt, move-constructor wäre halt schon gut in dem fall. Für sowas würde ich dann sogar den Copy-Constructor deleten oder zumindest als explicit deklarieren.



  • Trotz des schon gesagtem solltest du den Thread im Dtor joinen:

    {
      MyClass c;
    }
    // c wurde gelöscht, ggf noch bevor der Thread (der eine member-fkt aufruft) fertig ist
    


  • NoreSoft schrieb:

    Meine Frage ist nun: Wie verhindert man sowas "ordentlich"? Niemals im Konstruktor neue Threads starten?

    Nö. Ganz einfach: vergiss einfach dass es die Funktion detach() gibt.
    Der Thread wird dann zu nem Member, und wenn du dann vergisst den im dtor zu joinen, dann merkst du das ganz schnell beim Testen.
    => Tadaa, Problem gelöst.

    ps: Und natürlich: Objekte die Threads "besitzen" sind typischerweise non-copyable und non-movable.
    Wobei der Compiler das nicht unbedingt "sieht", d.h. du musst den Copy-Ctor und Move-Ctor u.U. selbst deleten.

    Und warum ist das so? Ganz einfach: weil der Thread mit nem fix in seinen Funktor reinkodierten "this" läuft, und daher darf sich "this" auch nicht ändern. => move nicht möglich.
    Und das Kopieren eines Objekts welches nen eigenen Thread hat ist meist schon semantisch völliger Quatsch.



  • Hallo zusammen,

    danke für die vielen schnellen Antworten. 🙂

    Also ich fasse nochmal kurz zusammen:

    1. detach() nicht mehr benutzen, stattdessen den Thread als Membervariable halten und spätestens im Dtor joinen.

    2. Copy-Ctor löschen, weil das Kopieren eines Objekts, das einen eigenen Thread hat, wenig Sinn macht.

    3. Move-Ctor entweder löschen oder darin den eigenen Thread "abhandeln" (das heißt joinen und gegebenenfalls neuen Thread starten).

    Für meine Zwecke würde ich den Move- und Copy-Ctor löschen, und um Fehlern beim impliziten Verschieben durch vector aus dem Weg zu gehen würde ich in dem Vektor nicht mehr die Objekte, sondern Zeiger darauf speichern und die Objekte entsprechend auf dem Heap anlegen und an geeigneter Stelle wieder freigeben.
    Klingt das eurer Meinung nach sinnvoll?

    Viele Grüße
    David



  • Vielleicht ist die Boost thread_group was für Dich.



  • @boosty
    Wie kommst du auf die Idee? Ich sehe hier nichts wo thread_group helfen könnte.

    @NoreSoft
    Ja, ich denke in diesem Fall wäre das sinnvoll.

    NoreSoft schrieb:

    um Fehlern beim impliziten Verschieben durch vector aus dem Weg zu gehen

    Wenn die Klasse keinen Copy-Ctor und keinen Move-Ctor hat, dann kannst du Objekte dieser Klasse sowieso nicht mehr in einen vector stecken. Probier's einfach aus, der Code darf nicht compilieren. Irgendwie logisch, der vector hat dann ja keine Möglichkeit mehr die Elemente rumzuschieben.

    NoreSoft schrieb:

    würde ich in dem Vektor nicht mehr die Objekte, sondern Zeiger darauf speichern und die Objekte entsprechend auf dem Heap anlegen und an geeigneter Stelle wieder freigeben.

    Ich würde dir empfehlen das auf jeden Fall nicht manuell zu machen.
    Entweder nimm einen speziellen Container ala ptr_vector<Foo> (Boost), oder mach einen vector<unique_ptr<Foo>> . Beide stellen sicher dass die Foo Objekte beim Löschen des Containers automatisch auch gelöscht werden.

    Das mit Hand zu machen ist immer fummelig, und wird noch viel fummeliger wenn man versucht auch sämtliche "Spezialfälle" abzudecken.
    Wie z.B. den "Spezialfall" dass hier beim Vergrössern des vector eine Exception fliegt (z.B. bad_alloc ):

    myVector.push_back(new Foo()); // potentielles LEAK!!!
    


  • hustbaer schrieb:

    Das mit Hand zu machen ist immer fummelig, und wird noch viel fummeliger wenn man versucht auch sämtliche "Spezialfälle" abzudecken.
    Wie z.B. den "Spezialfall" dass hier beim Vergrössern des vector eine Exception fliegt (z.B. bad_alloc ):

    myVector.push_back(new Foo()); // potentielles LEAK!!!
    
    myVector.reserve(myVector.size()+1);
    myVector.push_back(new Foo()); // leakt nie
    

    Wobei ich mir über bad_alloc eh keine Sorgen machen würde. Tritt das auf, hast du sowieso verloren. Ob da was leakt oder nicht ist dann egal.



  • @fummlus
    Mir ist schon klar wie man das beheben kann.
    Nur sind das alles dreckige Workarounds - so programmiert man einfach nicht.

    OK, klar, man kann so programmieren. Genau so wie man sich jeden Tag mitm Hammer auf den Kopp hauen kann. Es macht nur einfach keinen Sinn.

    fummlus schrieb:

    Wobei ich mir über bad_alloc eh keine Sorgen machen würde. Tritt das auf, hast du sowieso verloren. Ob da was leakt oder nicht ist dann egal.

    Same here. Die Sache um die es geht ist viel grundlegender als ein spezieller Fall. Die Überlegung "kann der Fall hier wirklich auftreten, bzw. könnte die restliche Anwendung überhaupt sinnvoll damit umgehen" ist in 99% der Fälle Zeitverschwendung.

    Gewöhnt euch an sauber zu programmieren, dann stellen sich diese Fragen erst gar nicht.


Anmelden zum Antworten