vs2019 cpp20 规范的线程头文件 <thread> 注释并探讨几个问题(上)

(1)学习线程,与学习其它容器一样,要多读 STL 库的源码。很多知识就显然而然的明白了。也不用死记硬背一些结论。上面上传了一份注释了一下的 源码。主要是补充泛型推导与函数调用链。基于注释后的源码探讨几个知识点。 STL 库的多线程,从语言上支持多线程编程,屏蔽了操作系统上的内核函数差异。
(2 探讨 一)结论一 : 能作为线程中的函数执行,该函数的形参应是值传递,或者是右值引用;不能是左值引用,否则编译不通过。举例如下图:

在这里插入图片描述

函数形参是左值引用则直接报错:

在这里插入图片描述

(3) 为什么会如此呢?这要从 STL 库的源码开始。虽然我理解的不深,欢迎一起探讨。首先看下 thread 类的数据结构:

在这里插入图片描述

接着要看看 thread 的构造函数:

在这里插入图片描述

接着进入 STL 中的源码:

在这里插入图片描述

接着看下在 thread 里是如何处理传递给待执行函数的实参的。从上图也可以看出,STL 用 tuple 元组来存储实参,但先用 decay_t 模板对实参的类型进行了处理。 decay 英文意思是衰减,消退,在代码里的含义,结合其定义,就是去除实参类型上的三种修饰符:const、volatile、reference,只保留关于实参的最基本的类型(这就导致了实参向 tuple 元组的值传递,若实参是对象类型,就会因此调用对象的 copy 构造函数),decay_t 的定义如下(c++ 学习的难点就在于泛型推导,语法规则不多,但运用语法自洽的编写大型的具有功能的程序段,就难了):

在这里插入图片描述

decay 需要的其它模板定义:

在这里插入图片描述

以及:

在这里插入图片描述

所以可见,在 thread 中构建的 tuple 元组,是根据实参类型构建的,将只包含最基本的不含引用 ,const ,volatile 等修饰符的类型。
tuple 元组包含了待调用的函数及其相关的参数 , tuple 元组最终被作为实参传递给全局函数 invoke ,由 invoke 分析 tuple 中的数据后,发起对子线程中待执行函数的调用
接着在下一行代码里构建元组,并用实参对其初始化,并用独占智能指针指向这个新创建的 tuple 元组。将来子线程独立运行时,就根据 tuple 这个实参,来为待执行函数中的形参赋值。这里根据函数的形参类型细分一下:
(a)若函数形参是对象的值,tuple 元组中也是已构建好的对象,则调用对象的移动构造函数;若 tuple 中实参是对象单参构造函数的形参类型,则隐式调用对象的单参构造函数,以创建待执行函数中的形参对象。
(b)若函数形参是对象的右值引用,tuple 中是已构建好的对象,则函数形参直接引用到 tuple 中的对象;若 tuple 中的实参是对象
单参构造函数的形参类型,则隐式调用对象的单参构造函数,并将函数形参引用到这个无名对象,这也是右值引用的语义。
(c) 若函数形参是 const A& a 这样的常量左值引用,则处理情况等同于右值引用。因为 c++ 语法允许 const 的左值引用引用到无名对象,因为反正也不会修改形参对象的值。

所以可以得出结论,线程中的函数,其形参是独一无二的,与 main 函数中创建线程时传递的实参没有任何关系。即使子线程中函数形参是引用类型,对形参的修改,也不会影响主线程中的实参对象的值,即源于此

在这里插入图片描述
------------------------->后来几天又用例子测试了下,上面关于“ tuple 元组就是线程中待执行函数的形参 ” 的结论不正确,有待修正。测例如下:

在这里插入图片描述

但如果线程中待执行函数的形参是右值引用呢,就会少一次参数对象的 copy 构造,这时候才是待执行函数的形参变量直接引用到 tuple 元组中的数据。 因为存在这传值与右值引用的两种情况,所以 tuple 元组的生存周期等同于线程。这后一个结论还是正确的。测试右值引用如下:

在这里插入图片描述

这里还要再次补充。图中显示,子线程中待执行函数的形参是传值时,引起两次形参的 copy 构造。这是因为图中对象 A ,没有为其定义 移动构造函数。故编译器在从 tuple元组构造待执行函数的形参时,以 copy 构造函数代替了移动构造函数。仅仅是代替。若为对象 A 定义了移动构造函数,会调用移动构造函数的(在依据 tuple 元组构造函数形参时),测试如下:

在这里插入图片描述

接着给出依据实参隐式调用对象的单参数构造函数以创建子线程中函数的形参对象的情形:

在这里插入图片描述

以及当函数形参为右值引用时,对象的构造函数调用情况如下:

在这里插入图片描述

---------------------------> 谢谢包涵,这些知识我也是第一遍学。以上点划线之间是后来几天的补充。

下面给出 _Get_invoke ,该函数很简单,就是给出线程中待执行函数的起始地址:

在这里插入图片描述

接着附上关于 全局 invoke 的源码与注释,很长,仅供参考。咱们还是要结合源代码,图片只是示意:

在这里插入图片描述

以及其它辅助理解的注释:

在这里插入图片描述

以及:

在这里插入图片描述

至此,咱们基本清楚了线程中待执行函数的启动流程,准备过程,以及函数形参是如何产生的。谢谢阅读。

(4 探讨二)接着谈下第二个知识点:涉及线程中待执行函数的形参的生存周期。显然,函数形参的生存周期可能很长,是伴随线程存在,伴随其函数存在的。但这些形参数据是在 thread 这个类的构造函数执行时分配到内存的。显然独占指针 _Decay_copied 指向的存储形参数据的 tuple 元组,不应随着 _Decay_copied 这个智能指针的生存周期结束而到期。这是从以下代码发现出来的:

在这里插入图片描述

以下给出独占指针的相关成员函数的定义:

在这里插入图片描述

(5 探讨三)第五条是第二天的补充:探讨给线程中待执行函数的传参时的隐式转换:测试代码如下:

在这里插入图片描述

因为 参数 3 是写在构造线程的代码行中的。所以不必要在主线程的地址空间中依据 3 来构造临时对象 A 来为线程执行准备参数。隐式构造的对象 A 直接存在于被构造的线程的地址空间中。反之,若代码这么写 : thread thr ( g , A( 3 ) ) , 则此临时对象也会在主线程 main 所在线程中产生一份。造成资源浪费和执行效率变慢。测试如下:

在这里插入图片描述

再补充一下同时使用对象的隐式构造和函数的参数传值时的情况( 即形参不使用右值引用 ):

在这里插入图片描述

谢谢阅读。剩余的 join ,detach 结合源码就好理解了。很短的源代码。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值