【问题标题】:C++ thread using function object, how are multiple destructors are called but not the constructors?C++线程使用函数对象,如何调用多个析构函数而不是构造函数?
【发布时间】:2020-03-22 15:55:27
【问题描述】:

请在下面找到代码sn-p:

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

我得到的输出是:

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

我很困惑如何调用地址为 0x7ffe27d1b06c 和 0x2029c28 的析构函数而没有调用构造函数?而第一个和最后一个构造函数和析构函数分别属于我创建的对象。

【问题讨论】:

  • 定义和检测你的 copy-ctor 和 move-ctor。
  • 嗯,明白了。由于我将对象传递给被调用的复制构造函数,我是否正确?但是,何时调用移动构造函数?

标签: c++ multithreading destructor


【解决方案1】:

您缺少检测复制构造和移动构造。对您的程序进行简单的修改将提供证据表明正在发生构造。

复制构造函数

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

输出(地址不同)

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

复制构造函数和移动构造函数

如果您提供移动 ctor,那么至少其中一个副本将是首选:

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

输出(地址不同)

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

引用包装

如果您想避免这些副本,您可以将您的可调用对象包装在参考包装器中 (std::ref)。由于您想在线程部分完成后使用t,这对您的情况是可行的。在实践中,您必须非常小心处理对调用对象的引用的线程,因为对象的生命周期必须至少与使用引用的线程一样长。

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

输出(地址不同)

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

请注意,即使我保留了 copy-ctor 和 move-ctor 重载,也没有调用它们,因为引用包装器现在是被复制/移动的东西;不是它引用的东西。此外,最后一种方法提供了您可能正在寻找的东西; t.x 回到 main 实际上已修改为 11。之前的尝试中没有。然而,这一点再怎么强调都不为过:要小心这样做。对象的生命周期是关键的


动起来,别无他法

最后,如果您不想像示例中那样保留t,您可以使用移动语义将实例直接发送到线程,沿途移动。

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

输出(地址不同)

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

在这里您可以看到对象已创建,对 said-same 的右值引用然后直接发送到std::thread::thread(),在那里它再次移动到它的最终静止位置,从那时起由线程拥有。不涉及复制者。实际的 dtors 是针对两个 shell 和最终目的地的具体对象。

【讨论】:

    【解决方案2】:

    至于您在 cmets 中发布的其他问题:

    什么时候调用移动构造函数?

    std::thread 的构造函数首先创建其第一个参数的副本(由decay_copy 创建)——这就是调用 复制构造函数 的地方。 (请注意,如果是 rvalue 参数,例如 thread t1{std::move(t)};thread t1{tFunc{}};,则将调用 move 构造函数。)

    decay_copy 的结果是一个临时,驻留在堆栈上。然而,由于decay_copy 是由一个调用线程 执行的,所以这个临时驻留在它的堆栈上并在std::thread::thread 构造函数的末尾被销毁。因此,临时本身不能被新创建的线程直接使用。

    要将仿函数“传递”到新线程,需要在其他地方创建一个新对象,这就是调用移动构造函数的地方。 (如果它不存在,将调用复制构造函数。)


    请注意,我们可能想知道为什么这里没有应用延迟临时实现。例如,在这个live demo 中,只调用了一个构造函数而不是两个。我认为 C++ 标准库实现的一些内部实现细节阻碍了 std::thread 构造函数的优化。

    【讨论】:

      猜你喜欢
      • 2016-03-25
      • 2022-11-21
      • 2019-12-16
      • 2011-04-16
      • 2015-06-30
      • 2013-04-17
      • 2021-07-19
      • 2021-07-28
      • 1970-01-01
      相关资源
      最近更新 更多