Thread C ++ che utilizza l'oggetto funzione, come vengono chiamati più distruttori ma non i costruttori?


15

Di seguito trovi lo snippet di codice:

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;
}

L'output che sto ottenendo è:

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

Sono confuso come hanno chiamato i distruttori con indirizzo 0x7ffe27d1b06c e 0x2029c28 e non sono stati chiamati costruttori? Considerando che rispettivamente il primo e l'ultimo costruttore e distruttore sono dell'oggetto che ho creato.


11
Definisci e strumentale anche il tuo copia-ctor e move-ctor.
WhozCraig,

Capito bene. Dato che sto passando l'oggetto chiamato dal costruttore della copia, ho ragione? Ma quando viene chiamato il costruttore di mosse?
SHAHBAZ,

Risposte:


18

Ti manca la strumentazione della copia-costruzione e della costruzione di movimento. Una semplice modifica al tuo programma fornirà la prova di dove stanno avvenendo le costruzioni.

Costruttore di copie

#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;
}

Uscita (gli indirizzi variano)

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

Copia costruttore e sposta costruttore

Se fornisci un mittente, sarà preferito per almeno una di quelle copie altrimenti:

#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;
}

Uscita (gli indirizzi variano)

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

Riferimento spostato

Se si desidera evitare tali copie, è possibile avvolgere il richiamo in un wrapper di riferimento ( std::ref). Dal momento che si desidera utilizzare tdopo aver terminato la parte di threading, questo è praticabile per la propria situazione. In pratica è necessario fare molta attenzione quando si esegue il threading con riferimenti a oggetti call, poiché la durata dell'oggetto deve prolungarsi almeno fino a quando il thread utilizza il riferimento.

#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;
}

Uscita (gli indirizzi variano)

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

Nota anche se ho mantenuto i sovraccarichi di copia-ctor e move-ctor, nessuno dei due è stato chiamato, poiché il wrapper di riferimento è ora l'oggetto da copiare / spostare; non la cosa a cui fa riferimento. Inoltre, questo approccio finale offre ciò che probabilmente stavi cercando; t.xil back in mainè, infatti, modificato in 11. Non era nei precedenti tentativi. Non posso sottolinearlo abbastanza, comunque: fai attenzione . La durata dell'oggetto è fondamentale .


Muoviti e nient'altro che

Infine, se non hai interesse a conservare tcome nel tuo esempio, puoi utilizzare la semantica di spostamento per inviare l'istanza direttamente al thread, spostandoti lungo la strada.

#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;
}

Uscita (gli indirizzi variano)

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

Qui puoi vedere l'oggetto creato, il riferimento al valore a detto-stesso quindi inviato direttamente a std::thread::thread(), dove viene nuovamente spostato nel suo punto di riposo finale, di proprietà del thread da quel punto in avanti. Nessun copy-ctors è coinvolto. I veri dottori sono contro due proiettili e l'oggetto concreto di destinazione finale.


5

Per quanto riguarda la tua domanda aggiuntiva pubblicata nei commenti:

Quando viene chiamato il costruttore di mosse?

Il costruttore di std::threadfirst crea una copia del suo primo argomento (by decay_copy) - è qui che viene chiamato il costruttore di copie . (Si noti che nel caso di un argomento rvalue , come thread t1{std::move(t)};o thread t1{tFunc{}};, si dovrebbe chiamare invece costruttore di movimento .)

Il risultato di decay_copyè un temporaneo che risiede nello stack. Tuttavia, poiché decay_copyviene eseguito da un thread chiamante , questo temporaneo risiede sul suo stack e viene distrutto alla fine del std::thread::threadcostruttore. Di conseguenza, il temporaneo stesso non può essere utilizzato direttamente da un nuovo thread creato.

Per "passare" il funzione al nuovo thread, è necessario creare un nuovo oggetto da qualche altra parte , ed è qui che viene invocato il costruttore di mosse . (Se non esistesse, verrà invece richiamato il costruttore di copie.)


Si noti che potremmo chiederci perché la materializzazione temporanea differita non viene applicata qui. Ad esempio, in questa demo live , viene invocato solo un costruttore anziché due. Ritengo che alcuni dettagli di implementazione interna dell'implementazione della libreria C ++ Standard ostacolino l'ottimizzazione da applicare per il std::threadcostruttore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.