C ++ - Thread mit Funktionsobjekt. Wie werden mehrere Destruktoren aufgerufen, nicht jedoch die Konstruktoren?

15

Das Code-Snippet finden Sie unten:

class tFunc{
    int x;
    public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }
    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX(){ return x; }
};

int main()
{
    tFunc t;
    thread t1(t);
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Die Ausgabe, die ich bekomme, ist:

Constructed : 0x7ffe27d1b0a4
Destroyed : 0x7ffe27d1b06c
Thread is joining...
Thread running at : 11
Destroyed : 0x2029c28
x : 1
Destroyed : 0x7ffe27d1b0a4

Ich bin verwirrt, wie die Destruktoren mit den Adressen 0x7ffe27d1b06c und 0x2029c28 aufgerufen wurden und keine Konstruktoren aufgerufen wurden. Während der erste und der letzte Konstruktor bzw. Destruktor von dem Objekt sind, das ich erstellt habe.

SHAHBAZ
quelle
11
Definieren und instrumentieren Sie auch Ihren Copy-Ctor und Move-Ctor.
WhozCraig
Gut verstanden. Bin ich richtig, da ich das Objekt übergebe, das der aufgerufene Kopierkonstruktor ist? Aber wann wird der Verschiebungskonstruktor aufgerufen?
SHAHBAZ

Antworten:

18

Ihnen fehlt die Instrumentenkopie- und Verschiebungskonstruktion. Eine einfache Änderung Ihres Programms liefert Hinweise darauf, wo die Konstruktionen stattfinden.

Konstruktor kopieren

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    tFunc t;
    thread t1{t};
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Ausgabe (Adressen variieren)

Constructed : 0x104055020
Copy constructed : 0x104055160 (source=0x104055020)
Copy constructed : 0x602000008a38 (source=0x104055160)
Destroyed : 0x104055160
Thread running at : 11
Destroyed : 0x602000008a38
Thread is joining...
x : 1
Destroyed : 0x104055020

Konstruktor kopieren und Konstruktor verschieben

Wenn Sie einen Verschiebungscode angeben, wird dieser für mindestens eine dieser ansonsten Kopien bevorzugt:

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    tFunc(tFunc&& obj) : x(obj.x)
    {
        cout<<"Move constructed : "<<this<< " (source=" << &obj << ')' << endl;
        obj.x = 0;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    tFunc t;
    thread t1{t};
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Ausgabe (Adressen variieren)

Constructed : 0x104057020
Copy constructed : 0x104057160 (source=0x104057020)
Move constructed : 0x602000008a38 (source=0x104057160)
Destroyed : 0x104057160
Thread running at : 11
Destroyed : 0x602000008a38
Thread is joining...
x : 1
Destroyed : 0x104057020

Referenz verpackt

Wenn Sie diese Kopien vermeiden möchten, können Sie Ihren Callable in einen Referenz-Wrapper ( std::ref) einschließen . Da Sie tnach dem Einfädeln den Teil verwenden möchten, ist dies für Ihre Situation sinnvoll. In der Praxis müssen Sie beim Threading gegen Referenzen zum Aufrufen von Objekten sehr vorsichtig sein, da die Lebensdauer des Objekts mindestens so lange dauern muss, wie der Thread die Referenz verwendet.

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    tFunc(tFunc&& obj) : x(obj.x)
    {
        cout<<"Move constructed : "<<this<< " (source=" << &obj << ')' << endl;
        obj.x = 0;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    tFunc t;
    thread t1{std::ref(t)}; // LOOK HERE
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    cout<<"x : "<<t.getX()<<endl;
    return 0;
}

Ausgabe (Adressen variieren)

Constructed : 0x104057020
Thread is joining...
Thread running at : 11
x : 11
Destroyed : 0x104057020

Beachten Sie, dass, obwohl ich die Überladungen copy-ctor und move-ctor beibehalten habe, keine aufgerufen wurden, da der Referenz-Wrapper jetzt das zu kopierende / verschobene Objekt ist. nicht das, worauf es sich bezieht. Dieser endgültige Ansatz liefert auch das, wonach Sie wahrscheinlich gesucht haben. t.xback in mainist in der Tat modifiziert zu 11. Es war nicht in den vorherigen Versuchen. Ich kann das jedoch nicht genug betonen: Sei vorsichtig dabei . Die Objektlebensdauer ist kritisch .


Bewegen Sie sich und nichts als

Wenn Sie kein Interesse daran thaben, wie in Ihrem Beispiel beizubehalten, können Sie die Verschiebungssemantik verwenden, um die Instanz direkt an den Thread zu senden und sich dabei zu bewegen.

#include <iostream>
#include <thread>
#include <functional>
using namespace std;

class tFunc{
    int x;
public:
    tFunc(){
        cout<<"Constructed : "<<this<<endl;
        x = 1;
    }
    tFunc(tFunc const& obj) : x(obj.x)
    {
        cout<<"Copy constructed : "<<this<< " (source=" << &obj << ')' << endl;
    }

    tFunc(tFunc&& obj) : x(obj.x)
    {
        cout<<"Move constructed : "<<this<< " (source=" << &obj << ')' << endl;
        obj.x = 0;
    }

    ~tFunc(){
        cout<<"Destroyed : "<<this<<endl;
    }

    void operator()(){
        x += 10;
        cout<<"Thread running at : "<<x<<endl;
    }
    int getX() const { return x; }
};

int main()
{
    thread t1{tFunc()}; // LOOK HERE
    if(t1.joinable())
    {
        cout<<"Thread is joining..."<<endl;
        t1.join();
    }
    return 0;
}

Ausgabe (Adressen variieren)

Constructed : 0x104055040
Move constructed : 0x104055160 (source=0x104055040)
Move constructed : 0x602000008a38 (source=0x104055160)
Destroyed : 0x104055160
Destroyed : 0x104055040
Thread is joining...
Thread running at : 11
Destroyed : 0x602000008a38

Hier können Sie sehen, dass das Objekt erstellt wird. Der r-Wert-Verweis auf das gleiche Objekt wird dann direkt an gesendet std::thread::thread(), wo es erneut an seinen endgültigen Ruheplatz verschoben wird, der dem Thread von diesem Punkt an gehört. Es sind keine Kopierer beteiligt. Die tatsächlichen Dtoren sind gegen zwei Schalen und das konkrete Endzielobjekt.

WhozCraig
quelle
5

Wie für Ihre zusätzliche Frage in den Kommentaren gepostet:

Wann wird der Verschiebungskonstruktor aufgerufen?

Der Konstruktor von std::threadfirst erstellt eine Kopie seines ersten Arguments (von decay_copy) - dort wird der Kopierkonstruktor aufgerufen. (Beachten Sie, dass im Falle eines rvalue Argument, wie thread t1{std::move(t)};oder thread t1{tFunc{}};, bewegen Konstruktor stattdessen aufgerufen werden.)

Das Ergebnis von decay_copyist eine temporäre Datei, die sich auf dem Stapel befindet. Da dies decay_copyjedoch von einem aufrufenden Thread ausgeführt wird , befindet sich dieser temporäre Thread auf seinem Stapel und wird am Ende des std::thread::threadKonstruktors zerstört. Folglich kann das Temporär selbst nicht von einem neu erstellten Thread direkt verwendet werden.

Um den Funktor an den neuen Thread zu "übergeben", muss ein neues Objekt an einer anderen Stelle erstellt werden , und hier wird der Verschiebungskonstruktor aufgerufen. (Wenn es nicht vorhanden wäre, würde stattdessen der Kopierkonstruktor aufgerufen.)


Beachten Sie, dass wir fragen sich vielleicht , warum latente temporäre Materialisierung hier nicht angewendet wird. In dieser Live-Demo wird beispielsweise nur ein Konstruktor anstelle von zwei aufgerufen. Ich glaube, dass einige interne Implementierungsdetails der Implementierung der C ++ Standard-Bibliothek die Anwendung für den std::threadKonstruktor behindern .

Daniel Langr
quelle