std::thread 的构造-源码解析

std::thread 的构造-源码解析

我们这单章是为了专门解释一下 std::thread 是如何构造的,是如何创建线程传递参数的,让你彻底了解这个类。

我们以 MSVC 实现的 std::thread 代码进行讲解。

std::thread 的数据成员

  • 了解一个庞大的类,最简单的方式就是先看它的数据成员有什么

std::thread 只保有一个私有数据成员 _Thr

private:_Thrd_t _Thr;

_Thrd_t 是一个结构体,它保有两个数据成员:

using _Thrd_id_t = unsigned int;
struct _Thrd_t { // thread identifier for Win32void* _Hnd; // Win32 HANDLE_Thrd_id_t _Id;
};

结构很明确,这个结构体的 _Hnd 成员是指向线程的句柄,_Id 成员就是保有线程的 ID。

在64 位操作系统,因为内存对齐,指针 8 ,无符号 int 4,这个结构体 _Thrd_t 就是占据 16 个字节。也就是说 sizeof(std::thread) 的结果应该为 16

std::thread 的构造函数

std::thread 有四个构造函数,分别是:

  1. 默认构造函数,构造不关联线程的新 std::thread 对象。

    thread() noexcept : _Thr{} {}
    

    值初始化了数据成员 _Thr ,这里的效果相当于给其成员 _Hnd_Id 都进行零初始化。

  2. 移动构造函数,转移线程的所有权,构造 other 关联的执行线程的 std::thread 对象。此调用后 other 不再表示执行线程失去了线程的所有权。

    thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {}
    

    _STD 是一个宏,展开就是 ::std::,也就是 ::std::exchange,将 _Other._Thr 赋为 {} (也就是置空),返回 _Other._Thr 的旧值用以初始化当前对象的数据成员 _Thr

  3. 复制构造函数被定义为弃置的,std::thread 不可复制。两个 std::thread 不可表示一个线程,std::thread 对线程资源是独占所有权。

    thread(const thread&) = delete;
    
  4. 构造新的 std::thread 对象并将它与执行线程关联。表示新的执行线程开始执行

    template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>_NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) {_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);}
    

前三个构造函数都没啥要特别聊的,非常简单,只有第四个构造函数较为复杂,且是我们本章重点,需要详细讲解。(注意 MSVC 使用标准库的内容很多时候不加 std::,脑补一下就行

如你所见,这个构造函数本身并没有做什么,它只是一个可变参数成员函数模板,增加了一些 SFINAE 进行约束我们传入的可调用对象的类型不能是 std::thread。函数体中调用了一个函数 _Start,将我们构造函数的参数全部完美转发,去调用它,这个函数才是我们的重点,如下:

template <class _Fn, class... _Args>
void _Start(_Fn&& _Fx, _Args&&... _Ax) {using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});_Thr._Hnd =reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));if (_Thr._Hnd) { // ownership transferred to the thread(void) _Decay_copied.release();} else { // failed to start thread_Thr._Id = 0;_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);}
}
  1. 它也是一个可变参数成员函数模板,接受一个可调用对象 _Fn 和一系列参数 _Args... ,这些东西用来创建一个线程。

  2. using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>

    • 定义了一个元组类型 _Tuple ,它包含了可调用对象和参数的类型,这里使用了 decay_t 来去除了类型的引用和 cv 限定。
  3. auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...)

    • 使用 make_unique 创建了一个独占指针,指向的是 _Tuple 类型的对象,存储了传入的函数对象和参数的副本。
  4. constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{})

    • 调用 _Get_invoke 函数,传入 _Tuple 类型和一个参数序列的索引序列(为了遍历形参包)。这个函数用于获取一个函数指针,指向了一个静态成员函数 _Invoke,用来实际执行线程。这两个函数都非常的简单,我们来看看:
     template <class _Tuple, size_t... _Indices>_NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept {return &_Invoke<_Tuple, _Indices...>;}template <class _Tuple, size_t... _Indices>static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ {// adapt invoke of user's callable object to _beginthreadex's thread procedureconst unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));_Tuple& _Tup = *_FnVals.get(); // avoid ADL, handle incomplete types_STD invoke(_STD move(_STD get<_Indices>(_Tup))...);_Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABIreturn 0;}
    

    _Get_invoke 函数很简单,就是接受一个元组类型,和形参包的索引,传递给 _Invoke 静态成员函数模板,实例化,获取它的函数指针。

    它的形参类型我们不再过多介绍,你只需要知道 index_sequence 这个东西可以用来接收一个由 make_index_sequence 创建的索引形参包,帮助我们进行遍历即可。

    _Invoke 是重中之重,它是线程实际执行的函数,如你所见它的形参类型是 void* ,这是必须的,要符合 _beginthreadex 执行函数的类型要求。虽然是 void*,但是我可以将它转换为 _Tuple* 类型,构造一个独占智能指针,然后用 get 成员函数获取底层指针,解引用,获取引用,_Tup 接取。

    此时,我们就可以进行调用了,使用 std::invoke + std::move(默认移动) ,这里有一个形参包展开,_STD get<_Indices>(_Tup))...,_Tup 就是 std::tuple 的引用,我们使用 std::get<> 获取元组存储的数据,需要传入一个索引,这里就用到了 _Indices。展开之后,就等于 invoke 就接受了我们构造 std::thread 传入的可调用对象,调用可调用对象的参数,invoke 就可以执行了。

  5. _Thr._Hnd = reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id))

    • 调用 _beginthreadex 函数来启动一个线程,并将线程句柄存储到 _Thr._Hnd 中。传递给线程的参数为 _Invoker_proc(一个静态函数指针,就是我们前面讲的 _Invoke)和 _Decay_copied.get()(存储了函数对象和参数的副本的指针)。
  6. if (_Thr._Hnd) {

    • 如果线程句柄 _Thr._Hnd 不为空,则表示线程已成功启动,将独占指针的所有权转移给线程。
  7. (void) _Decay_copied.release()

    • 释放独占指针的所有权,因为已经将参数传递给了线程。
  8. } else { // failed to start thread

    • 如果线程启动失败,则进入这个分支
  9. _Thr._Id = 0;

    • 将线程ID设置为0。
  10. _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);

  • 抛出一个 C++ 错误,表示资源不可用,请再次尝试。

总结

需要注意,libstdc++ 和 libc++ 可能不同,就比如它们 64位环境下 sizeof(std::thread) 的结果就是 8,它们的实现只保有一个线程 ID。

我们这里的源码解析涉及到的 C++ 技术很多,我们也没办法每一个都单独讲,那会显得文章很冗长,而且也不是重点。

相信你也感受到了,不会模板,你阅读标准库源码,是无稽之谈,市面上很多教程教学,教导一些实现容器,过度简化了,真要去出错了去看标准库的代码,那是不现实的。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:现代C++模板教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/738856.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Anaconda环境全局环境手动配置

Anaconda环境全局环境手动配置 如图&#xff0c;在本机就有Anaconda的情况下&#xff0c;普通cmd无法直接使用conda命令&#xff0c;每次都要从Anaconda Prompt窗口操作&#xff0c;挺不方便的&#xff0c;遂决定进行全局环境配置&#xff0c;记录一下流程。 找到环境配置页面…

R语言绘制散点密度图ggdentity

使用R语言绘制二维密度图 下图是一张常见的二维核密度散点图&#xff0c;能够清晰直观的反映出数据之间的分布趋势&#xff0c;颜色越红的位置数据越集中分布。今天分享的笔记是在R语言中绘制该图的两种常见方法&#xff0c;提供过程代码。 论文中常见的这种展示两组数据之间分…

Java入门学习(1)

常用的CMD命令&#xff1a; 1.盘符名称 冒号 说明&#xff1a;盘符切换&#xff1b; 举例&#xff1a;E:回车&#xff0c;表示切换到E盘。 2.dir 说明&#xff1a;查看当前路径下的内容。 3.cd 说明&#xff1a;进入单级目录。 举例&#xff1a;cd itheima 4.cd.. 说…

上班族必备技能!轻松掌握excel文件如何批量加后缀名方法,提升工作效率!

后缀名是什么 后缀名是帮助人区分文件类型的一种手段&#xff0c;文件后缀名也叫文件扩展名&#xff0c;是用来表示某种文件格式所采用的机制。文件扩展名是加在主文件名后面的&#xff0c;用“.”分隔。不同的软件要求不同的文件格式&#xff0c;后缀名可以帮助用户了解文件是…

可以作为GC.Roots的对象有哪些?

在Java中&#xff0c;GC Roots 是一组特殊的对象&#xff0c;它们被认为是可达的&#xff0c;并且不会被垃圾收集器回收。这些对象包括但不限于以下几种&#xff1a; 虚拟机栈中引用的对象&#xff1a;活跃线程中的本地变量引用的对象&#xff0c;以及正在执行的方法中的参数对…

Python 可视化和数据缺失处理库之missingno使用详解

概要 在数据分析和数据科学的领域中,数据缺失是一个常见的问题。数据缺失可能会导致分析和建模结果的不准确性,因此了解如何处理和可视化缺失数据至关重要。Python Missingno 是一个强大的工具,可以直观地识别和处理数据中的缺失值。本文将详细介绍 Python Missingno 的功能…

从命令提示窗口使用 Visual C++ Toolkit 2003

从命令提示窗口使用 Visual C Toolkit 2003 发布日期 : 12/20/2004 | 更新日期 : 12/20/2004 Brian Johnson MSDN Visual C 内容战略家 适用于&#xff1a; Visual Studio .NET 2003 Microsoft Visual C .NET 2003 Microsoft Visual C Toolkit 2003 摘要&#xff1a;在本文中…

Threejs播放模型自带动画

现在的很多建模软件都可以制作动画效果&#xff0c;甚至可以通过各种动画效果直接做动漫&#xff0c;动漫是模型的一种属性&#xff0c;在threejs中同样可以加载此动画&#xff0c;实现动画效果&#xff0c;如果有的时候在threejs中用代码实现模型动画比较困难或者麻烦&#xf…

MongoDB聚合运算符:$denseRank

$denseRank聚合运算符返回在$setWindowFields阶段分区中文档的排名&#xff0c;排名的顺序由$setWindowFields阶段sortBy的字段值决定。 语法 { $denseRank: { } }$denseRank不需要任何参数。 使用 $rank和$denseRank的不同点在于他们处理排序字段重复值的方式不同&#xf…

嵌入式、开发板 智能音响 OpenHarmony GPT 大模型 智能硬件

一、概述 生活场景的引入: 物联网的快速发展,各种智能设备层出不穷,作为极客,家里早已安上了用 APP 控制的智能灯、智能插座,刚刚安装上的时候,还有新鲜感,久了之后,是不是会有这样的现象: 早上醒来要开灯,需要经过: 迷迷糊糊从床头柜上摸到手机手机用指纹解锁连接无…

3.11 log | 739. 每日温度,

739. 每日温度&#xff0c;496.下一个更大元素 I&#xff0c;503.下一个更大元素II&#xff0c; class Solution { public:vector<int> dailyTemperatures(vector<int>& temperatures) {stack<int> st;vector<int> result(temperatures.size(),0)…

Hadoop学习1:概述、单体搭建、伪分布式搭建

文章目录 概述基础知识Hadoop组件构成Hadoop配置文件 环境准备配置Hadoop配置下载配置环境变量 Hadoop运行模式Standalone Operation&#xff08;本地&#xff09;官方DemoWordCount单词统计Demo Pseudo-Distributed Operation&#xff08;伪分布式模式&#xff09;配置修改启动…

Bootstrap5(display显示、flex布局相关属性、浮动、定位、文本、栅格系统)

类中缀的设置技巧 1.当多个连续品目使用一个样式时&#xff0c;则给最小的设置即可。 比如&#xff1a;大屏以上内边距都是3&#xff1a;p-lh-3 2.超小屏不设置类中缀的样式 比如超小屏内边距时1&#xff0c;小屏内边距时2&#xff0c;中屏及以上内边距是3 p-1 p-sm-2 p-md-3 …

WPF 界面刷新问题 不支持从调度程序线程以外的线程对其 SourceCollection 进行的更改

WPF 界面刷新问题 不支持从调度程序线程以外的线程对其 SourceCollection 进行的更改 问题描述&#xff1a; 在子线程中操作界面控件的数据源出现以下错误&#xff1a;System.NotSupportedException:“该类型的 CollectionView 不支持从调度程序线程以外的线程对其 SourceCo…

【Python】新手入门学习:什么是相对路径?

【Python】新手入门学习&#xff1a;什么是相对路径&#xff1f; &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得…

【Echarts】柱状图上方显示数字以及自定义值,标题和副标题居中,鼠标上显示信息以及自定义信息

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是《前端》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的理解和掌握…

linux 环境安装nvm

linux 环境安装nvm 1、安装方式 # 方式1 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 方式2 【推荐】 wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash2、创建nvm命令目录 mkdir -p ~/.nvm3、编…

StringBuilder --java学习笔记

StringBuilder 代表可变字符串对象&#xff0c;相当于是一个容器&#xff0c;它里面装的字符串是可以改变的&#xff0c;就是用来操作字符串的StringBuilder比String更适合做字符串的修改操作&#xff0c;效率会更高&#xff0c;代码也会更简洁 StringBuilder的常用构造器和方…

C++操作树莓派的RTC时钟

概述 RTC实时时钟通常是指一个集成电路&#xff0c;RTC本质上是一个独立的定时器&#xff0c;通常情况下需要外接一个32.768KHZ的晶振和匹配电容&#xff08;10~33pf&#xff09;&#xff0c;由于时间是不停止的&#xff0c;为了满足这一要求&#xff0c;所以RTC实时时钟有两种…

idea2023和历史版本的下载

1.idea中文官网 idea官网历史版本下载(https://www.jetbrains.com.cn/idea/download/other.html)