c++中std::thread构造函数的注意事项

目录

一、问题引出

二、示例代码及输出结果

三、详细解释

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++中std::thread构造函数的注意事项_第1张图片

本文只讲第三个形式。

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行输出印证了这一观点。

以下是对这一段代码的详细解释。

三、详细解释

1.关键点解析

通过代码和输出,可以明确三次拷贝构造的来源及析构顺序:


1.1 第一次拷贝构造:临时对象(mData=101

  • 触发时机:当调用 std::thread 构造函数时,参数 foo1 需要被传递到线程的内部存储。

  • 具体过程

    • 参数 foo1 是左值,需通过 decay-copy 生成一个临时副本。

    • 此处触发第一次拷贝构造函数:

      Foo(const Foo& foo):0x7fffd04454a0 :101
  • 析构时机

    • 这个临时对象在 std::thread 构造函数完成后立即析构(因为它仅用于初始化线程的内部存储)。

    • 对应输出中的析构顺序:

      ~Foo():0x7fffd04454a0 :101  // 临时对象析构
      ~Foo():0x7fffd0445520 :100  // 原始对象析构

1.2 第二次拷贝构造:线程内部存储对象(mData=102

  • 触发时机:线程启动时,需要将参数从主线程传递到新线程的上下文。

  • 具体过程

    • 临时对象(mData=101)会被移动(或拷贝,若无移动语义)到线程的内部存储。

    • 由于 Foo 未定义移动构造函数,此处触发第二次拷贝构造函数:

      Foo(const Foo& foo):0x55a08cdd72c8 :102
  • 生命周期

    • 该对象存储在线程内部,直到线程执行完毕才会析构。

    • 对应输出中的析构顺序:

      ~Foo():0x55a08cdd72c8 :102  // 线程内部对象析构(在 `t1.join()` 之后)

1.3 第三次拷贝构造:线程函数参数 p4mData=103

  • 触发时机:线程函数 threadFunc 的参数 p4 是按值传递的。

  • 具体过程

    • 线程内部存储的对象(mData=102)需要拷贝到 p4 中。

    • 触发第三次拷贝构造函数:

      Foo(const Foo& foo):0x7f5b04383d8c :103
  • 生命周期

    • p4 在 threadFunc 执行结束时析构。

    • 对应输出中的析构顺序:

      ~Foo():0x7f5b04383d8c :103  // 函数参数析构

2.析构顺序验证

  • 原始对象 foo1mData=100

    • 在其所在块作用域结束时析构({ ... Foo foo1(100); ... } 结束)。

  • 临时对象(mData=101

    • 在 std::thread 构造函数完成后立即析构。

  • 线程内部存储对象(mData=102

    • 在 t1.join() 后析构(线程完全结束时)。

  • 函数参数 p4mData=103

    • 在 threadFunc 执行结束时析构。

输出结果与上述逻辑完全一致:

~Foo():0x7fffd04454a0 :101  // 临时对象析构
~Foo():0x7fffd0445520 :100  // 原始对象析构
~Foo():0x7f5b04383d8c :103  // 函数参数析构
~Foo():0x55a08cdd72c8 :102  // 线程内部存储对象析构

3.结论

  1. 第一次拷贝构造是为了生成临时对象,用于初始化线程内部存储。

  2. 第二次拷贝构造是将临时对象移动到线程的内部存储(由于缺乏移动语义,退化为拷贝)。

  3. 第三次拷贝构造是将线程内部存储的对象传递给函数参数 p4(按值传递)。

析构顺序由对象的生命周期决定:

  • 临时对象和原始对象在主线程析构。

  • 线程内部存储对象在线程结束后析构。

  • 函数参数在函数结束时析构。


4.验证构造和析构发生在哪个线程

将代码稍作修改,增加打印线程id。看看每一次构造和每一次析构Foo对象是发生在哪一个线程中。

c++中std::thread构造函数的注意事项_第2张图片

#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而创建的临时对象。


5.看给Foo添加移动构造函数后的效果

按照五法则(Rule of five)给Foo添加移动构造函数,移动赋值运算符,拷贝赋值运算符。并打印源对象地址,新建对象this指针的值,便于查看对象构造的细节。

c++中std::thread构造函数的注意事项_第3张图片

#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调用的是移动构造函数。

你可能感兴趣的:(c++,开发语言)