目录
一、问题引出
二、示例代码及输出结果
三、详细解释
1.关键点解析
1.1 第一次拷贝构造:临时对象(mData=101)
1.2 第二次拷贝构造:线程内部存储对象(mData=102)
1.3 第三次拷贝构造:线程函数参数 p4(mData=103)
2.析构顺序验证
3.结论
4.验证构造和析构发生在哪个线程
5.看给Foo添加移动构造函数后的效果
函数原型详见
https://en.cppreference.com/w/cpp/thread/thread/thread
本文只讲第三个形式。
c++标准规定
根据C++的标准,当使用std::thread创建线程时,所有的参数都会被拷贝到线程的内部存储中,然后再传递给线程函数。这是因为线程可能在参数所在的作用域结束后才执行,所以必须确保参数的生存期足够长。所以,当传递对象作为参数时,会进行一次拷贝构造,创建该对象的副本,存储在线程的内部。
为了验证上述规则。写一段示例代码看看。
代码:
#include
#include
#include
class Foo{
public:
Foo(int d):mData(d){
std::cout<<"Foo():"<
输出结果:
Foo():0x7fffd0445520 :100
Foo(const Foo& foo):0x7fffd04454a0 :101
Foo(const Foo& foo):0x55a08cdd72c8 :102
~Foo():0x7fffd04454a0 :101
~Foo():0x7fffd0445520 :100
Foo(const Foo& foo):0x7f5b04383d8c :103
12 ,1.23 ,test string para ,0x7f5b04383d8c:103
~Foo():0x7f5b04383d8c :103
~Foo():0x55a08cdd72c8 :102
exit
先看内层块作用域的float fd变量。当内层块作用域结束之后,foo1和fd将失效,从第7行输出可以看到最终线程t1中仍然打印出了1.23,即正确的原来的fd的值。这就说明在构造t1时fd被拷贝到了t1线程内部。
再看foo1对象。原始foo1对象的析构是在内层块作用域结束时发生的,打印输出了第5行。从输出的第2行可以看到在构造线程t1时,确实是先发生了对foo1的拷贝构造。这印证了c++标准中的规定。
更多疑惑:为什么会发生三次拷贝构造?
第4行和第5行输出,表明在原始的foo1对象被析构之前先析构了第一次拷贝构造的对象(101)。这是为什么呢?如果第一次拷贝构造得到的对象(101)是线程内部存储的对象的话,那这个对象不应该这么早就被析构掉,而是应该跟随线程t1的生命周期在外层块作用域结束时被析构。所以有理由认为第一次拷贝构造得到的对象是一个临时对象,第二次拷贝构造得到的对象(102)才是线程内部存储的对象。第9行输出印证了这一观点。
以下是对这一段代码的详细解释。
通过代码和输出,可以明确三次拷贝构造的来源及析构顺序:
mData=101
)触发时机:当调用 std::thread
构造函数时,参数 foo1
需要被传递到线程的内部存储。
具体过程:
参数 foo1
是左值,需通过 decay-copy
生成一个临时副本。
此处触发第一次拷贝构造函数:
Foo(const Foo& foo):0x7fffd04454a0 :101
析构时机:
这个临时对象在 std::thread
构造函数完成后立即析构(因为它仅用于初始化线程的内部存储)。
对应输出中的析构顺序:
~Foo():0x7fffd04454a0 :101 // 临时对象析构
~Foo():0x7fffd0445520 :100 // 原始对象析构
mData=102
)触发时机:线程启动时,需要将参数从主线程传递到新线程的上下文。
具体过程:
临时对象(mData=101
)会被移动(或拷贝,若无移动语义)到线程的内部存储。
由于 Foo
未定义移动构造函数,此处触发第二次拷贝构造函数:
Foo(const Foo& foo):0x55a08cdd72c8 :102
生命周期:
该对象存储在线程内部,直到线程执行完毕才会析构。
对应输出中的析构顺序:
~Foo():0x55a08cdd72c8 :102 // 线程内部对象析构(在 `t1.join()` 之后)
p4
(mData=103
)触发时机:线程函数 threadFunc
的参数 p4
是按值传递的。
具体过程:
线程内部存储的对象(mData=102
)需要拷贝到 p4
中。
触发第三次拷贝构造函数:
Foo(const Foo& foo):0x7f5b04383d8c :103
生命周期:
p4
在 threadFunc
执行结束时析构。
对应输出中的析构顺序:
~Foo():0x7f5b04383d8c :103 // 函数参数析构
原始对象 foo1
(mData=100
):
在其所在块作用域结束时析构({ ... Foo foo1(100); ... }
结束)。
临时对象(mData=101
):
在 std::thread
构造函数完成后立即析构。
线程内部存储对象(mData=102
):
在 t1.join()
后析构(线程完全结束时)。
函数参数 p4
(mData=103
):
在 threadFunc
执行结束时析构。
输出结果与上述逻辑完全一致:
~Foo():0x7fffd04454a0 :101 // 临时对象析构
~Foo():0x7fffd0445520 :100 // 原始对象析构
~Foo():0x7f5b04383d8c :103 // 函数参数析构
~Foo():0x55a08cdd72c8 :102 // 线程内部存储对象析构
第一次拷贝构造是为了生成临时对象,用于初始化线程内部存储。
第二次拷贝构造是将临时对象移动到线程的内部存储(由于缺乏移动语义,退化为拷贝)。
第三次拷贝构造是将线程内部存储的对象传递给函数参数 p4
(按值传递)。
析构顺序由对象的生命周期决定:
临时对象和原始对象在主线程析构。
线程内部存储对象在线程结束后析构。
函数参数在函数结束时析构。
将代码稍作修改,增加打印线程id。看看每一次构造和每一次析构Foo对象是发生在哪一个线程中。
#include
#include
#include
class Foo{
public:
Foo(int d):mData(d){
std::cout<<"Foo():"<
输出结果
start. main thread id is:139983078192960
Foo():0x7ffd5d1297a0 :100 thread id is:139983078192960
Foo(const Foo& foo):0x7ffd5d129720 :101 thread id is:139983078192960
Foo(const Foo& foo):0x5556d0c222c8 :102 thread id is:139983078192960
~Foo():0x7ffd5d129720 :101 thread id is:139983078192960
~Foo():0x7ffd5d1297a0 :100 thread id is:139983078192960
Foo(const Foo& foo):0x7f5059a65d8c :103 thread id is:139983078188800
12 ,1.23 ,test string para ,0x7f5059a65d8c:103
~Foo():0x7f5059a65d8c :103 thread id is:139983078188800
~Foo():0x5556d0c222c8 :102 thread id is:139983078188800
exit. main thread id is:139983078192960
可以看出原始对象foo1和第一次拷贝构造得到的对象(101)都是在主线程中构造和析构的。再看第三次拷贝构造得到的对象(103),其构造和析构都是在子线程t1中发生的。与众不同的是,第二次拷贝构造得到的对象(102),其构造是发生在主线程中(输出第4行),析构是发生在子线程中(输出第10行)。这进一步说明了第一次拷贝构造的对象(101)是用于构造子线程t1而创建的临时对象。
按照五法则(Rule of five)给Foo添加移动构造函数,移动赋值运算符,拷贝赋值运算符。并打印源对象地址,新建对象this指针的值,便于查看对象构造的细节。
#include
#include
#include
class Foo{
public:
Foo(int d):mData(d){
std::cout<<"Foo():"<
输出:
start. main thread id is:140280682874688
Foo():0x7ffda7e4bd30 :100 thread id is:140280682874688
Foo(const Foo& foo):From foo(0x7ffda7e4bd30) construct this(0x7ffda7e4bcb0). mData(101). thread id is:140280682874688
Foo(Foo&& foo):From foo(0x7ffda7e4bcb0) construct this(0x5629901b62c8). mData(102). thread id is:140280682874688
~Foo():0x7ffda7e4bcb0 :101 thread id is:140280682874688
~Foo():0x7ffda7e4bd30 :100 thread id is:140280682874688
Foo(Foo&& foo):From foo(0x5629901b62c8) construct this(0x7f95a4456d8c). mData(103). thread id is:140280682870528
12 ,1.23 ,test string para ,0x7f95a4456d8c:103
~Foo():0x7f95a4456d8c :103 thread id is:140280682870528
~Foo():0x5629901b62c8 :102 thread id is:140280682870528
exit. main thread id is:140280682874688
可以看出给Foo添加移动构造函数之后,线程内部存储的对象(102)是对第一次拷贝构造得到的临时对象(101)调用移动构造函数得到的(输出第4行),线程函数内使用的对象(103)是对线程内部存储的对象(102)调用移动构造函数得到的(输出第7行)。
从输出第3行可以看出,t1 = std::thread(threadFunc,12,fd,"test string para",foo1);构造子线程t1时拷贝foo1对象的时候仍然调用的是拷贝构造函数,因为foo1是左值。
把代码第58行t1 = std::thread(threadFunc,12,fd,"test string para",foo1);改成t1 = std::thread(threadFunc,12,fd,"test string para",std::move(foo1));。输出为:
start. main thread id is:140377552844608
Foo():0x7ffd39b86ad0 :100 thread id is:140377552844608
Foo(Foo&& foo):From foo(0x7ffd39b86ad0) construct this(0x7ffd39b86a50). mData(101). thread id is:140377552844608
Foo(Foo&& foo):From foo(0x7ffd39b86a50) construct this(0x5641968fd2c8). mData(102). thread id is:140377552844608
~Foo():0x7ffd39b86a50 :101 thread id is:140377552844608
~Foo():0x7ffd39b86ad0 :100 thread id is:140377552844608
Foo(Foo&& foo):From foo(0x5641968fd2c8) construct this(0x7fac322bdd8c). mData(103). thread id is:140377552840448
12 ,1.23 ,test string para ,0x7fac322bdd8c:103
~Foo():0x7fac322bdd8c :103 thread id is:140377552840448
~Foo():0x5641968fd2c8 :102 thread id is:140377552840448
exit. main thread id is:140377552844608
此时,输出第3行表明t1 = std::thread(threadFunc,12,fd,"test string para",std::move(foo1));中构造子线程t1时,对foo1调用的是移动构造函数。