std::thread merkwürdige Effekte



  • Kann mir mal bitte jemand die Effekte erklären, die dieser Sourcecode mit sich bringt:

    #include <iostream>
    #include <thread>
    
    class TestTask
    {
    public:
        TestTask() { std::cout << "start " << std::endl; }
        virtual ~TestTask() { std::cout << "end" << std::endl; }
    
        void operator()() { std::cout << "run" << std::endl; }
    };
    
    int main()
    {
       TestTask tt;
       std::thread th(tt);
       th.join();
    }
    
    start
    end
    run
    end
    end
    
    Process returned 0 (0x0)
    Press any key to continue.
    

    Ich bin durch Zufall beim Rumspielen drauf gestoßen und kann es mir aktuell nicht so richtig erklären.

    Gebaut mit

    === TDM-GCC Compiler Suite for Windows ===
    --- GCC 4.9 Series ---
    *** Standard MinGW 32-bit Edition ***



  • So funktioniert es...

    #include <iostream>
    #include <thread>
    
    class TestTask
    {
    public:
        TestTask() { std::cout << "start " << std::endl; }
        virtual ~TestTask() { std::cout << "end" << std::endl; }
    
        void run() { std::cout << "run" << std::endl; }
    };
    
    void testrun( TestTask *tt )
    {
        tt->run();
    }
    
    int main()
    {
       TestTask tt;
       std::thread th( testrun, &tt );
       th.join();
    }
    

    aber warum geht die obere Variante nicht?



  • Denke auch an die Konstruktoren, die der Compiler für dich automatisch generiert, wie z.B. den Copy Constructor.
    Implementiere den mal selbst, gib dort ebenfalls "start" aus und teste es nochmal mit der ersten Variante.
    Ich denke deine Verwunderung wird sich dann wahrscheinlich einem Aha-Erlebnis auflösen 😉



  • Auf die Idee bin ich auch gerade gekommen:

    #include <iostream>
    #include <thread>
    
    class TestTask
    {
    public:
        TestTask() { std::cout << "start " << std::endl; }
        virtual ~TestTask() { std::cout << "end" << std::endl; }
        TestTask( const TestTask &rhs ) { std::cout << "copy-constr" << std::endl; }
        virtual TestTask &operator=( const TestTask & ) { std::cout << "zuweisung" << std::endl; }
    
        void operator()() { std::cout << "run" << std::endl; }
    };
    
    int main()
    {
       TestTask tt;
       std::thread th( tt );
       th.join();
    }
    

    gibt dann:

    start
    copy-constr
    copy-constr
    end
    run
    end
    end
    
    Process returned 0 (0x0)
    Press any key to continue.
    

    std::thread übernimmt doch eigentlich Funktionen als Parameter, d.h. er muss den operator() quasi als Funktion in dem Fall annehmen. Wieso fummelt er da noch mit dem Objekt selber rum?

    Ich hab mal nen Breakpoint in dem Copyconstruktor gesetzt.
    Beim zweiten Aufruf des CopyCtr ist er schon 20 Frames tief in der STL drin... D.h. er schleppt hier das Objekt noch 20 Meter weiter, bevor nen Funktionsaufruf erzeugt?



  • It0101 schrieb:

    std::thread übernimmt doch eigentlich Funktionen als Parameter, d.h. er muss den operator() quasi als Funktion in dem Fall annehmen. Wieso fummelt er da noch mit dem Objekt selber rum?

    Damit std::thread den operator() aufrufen kann, muss er das gesamte TestTask -Objekt haben. Ohne Objekt kann er schließlich auch keine Member-Funktionen aufrufen. Folglich muss std::thread eine Kopie von tt intern abspeichern. Stell dir mal vor, std::thread würde keine interne Kopie anlegen und du würdest std::thread mit einem rvalue initialisieren z.B. std::thread(TestTask{}) ; das ergäbe UB!

    Ferner ist festzuhalten, dass std::thread den (potentiell günstigeren) Move-Konstruktor verwenden würde, hättest du diesen nicht implizit deaktiviert durch die Definition des Copy-Konstruktors.

    LG



  • Danke. Prinzip ist jetzt verstanden 😉



  • All die Argumente des thread-Konstruktors werden kopiert. Für den Fall, dass das nicht das ist, was man will, gibt es std::ref und std::cref:

    #include <iostream>
    #include <thread>
    #include <functional>  // <-- hinzugefuegt
    
    class TestTask
    {
    public:
        TestTask() { std::cout << "start " << std::endl; }
        ~TestTask() { std::cout << "end" << std::endl; }
        TestTask( const TestTask &rhs ) { std::cout << "copy-constr" << std::endl; }
        TestTask &operator=( const TestTask & ) { std::cout << "zuweisung" << std::endl; return *this; }
    
        void operator()() { std::cout << "run" << std::endl; }
    };
    
    int main()
    {
       TestTask tt;
       std::thread th( std::ref(tt) );
       th.join(); // <-- ist jetzt super wichtig, weil sonst tt zu früh
                  //     zerstoert werden koennte, bevor th fertig ist.
    }
    


  • Es wird nicht zwingend kopiert, vorausgesetzt ein move constructor ist vorhanden:

    std::thread th(TestTask{});
    

    Genau so sollte man das auch machen - es sei denn, du brauchst dieses Objekt tatsächlich aus irgendwelchen Gründen im Hauptthread.


Log in to reply