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
有四个构造函数,分别是:
-
默认构造函数,构造不关联线程的新 std::thread 对象。
thread() noexcept : _Thr{} {}
值初始化了数据成员 _Thr ,这里的效果相当于给其成员
_Hnd
和_Id
都进行零初始化。 -
移动构造函数,转移线程的所有权,构造 other 关联的执行线程的
std::thread
对象。此调用后 other 不再表示执行线程失去了线程的所有权。thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {}
_STD 是一个宏,展开就是
::std::
,也就是::std::exchange
,将_Other._Thr
赋为{}
(也就是置空),返回_Other._Thr
的旧值用以初始化当前对象的数据成员_Thr
。 -
复制构造函数被定义为弃置的,std::thread 不可复制。两个 std::thread 不可表示一个线程,std::thread 对线程资源是独占所有权。
thread(const thread&) = delete;
-
构造新的
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);}
}
-
它也是一个可变参数成员函数模板,接受一个可调用对象
_Fn
和一系列参数_Args...
,这些东西用来创建一个线程。 -
using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>
- 定义了一个元组类型
_Tuple
,它包含了可调用对象和参数的类型,这里使用了decay_t
来去除了类型的引用和 cv 限定。
- 定义了一个元组类型
-
auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...)
- 使用
make_unique
创建了一个独占指针,指向的是_Tuple
类型的对象,存储了传入的函数对象和参数的副本。
- 使用
-
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 就可以执行了。 - 调用
-
_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()
(存储了函数对象和参数的副本的指针)。
- 调用
-
if (_Thr._Hnd) {
- 如果线程句柄
_Thr._Hnd
不为空,则表示线程已成功启动,将独占指针的所有权转移给线程。
- 如果线程句柄
-
(void) _Decay_copied.release()
- 释放独占指针的所有权,因为已经将参数传递给了线程。
-
} else { // failed to start thread
- 如果线程启动失败,则进入这个分支
-
_Thr._Id = 0;
- 将线程ID设置为0。
-
_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
- 抛出一个 C++ 错误,表示资源不可用,请再次尝试。
总结
需要注意,libstdc++ 和 libc++ 可能不同,就比如它们 64位环境下 sizeof(std::thread)
的结果就是 8,它们的实现只保有一个线程 ID。
我们这里的源码解析涉及到的 C++ 技术很多,我们也没办法每一个都单独讲,那会显得文章很冗长,而且也不是重点。
相信你也感受到了,不会模板,你阅读标准库源码,是无稽之谈,市面上很多教程教学,教导一些实现容器,过度简化了,真要去出错了去看标准库的代码,那是不现实的。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:现代C++模板教程。