|
| 1 | +# `std::thread` 的构造-源码解析 |
| 2 | + |
| 3 | +我们这单章是为了专门解释一下 `std::thread` 是如何构造的,是如何创建线程传递参数的,让你彻底了解这个类。 |
| 4 | + |
| 5 | +我们以 **MSVC** 实现的 [`std::thread`](https://github.com/microsoft/STL/blob/main/stl/inc/thread) 代码进行讲解。 |
| 6 | + |
| 7 | +## `std::thread` 的数据成员 |
| 8 | + |
| 9 | +- **了解一个庞大的类,最简单的方式就是先看它的数据成员有什么**。 |
| 10 | + |
| 11 | +`std::thread` 只保有一个私有数据成员 [`_Thr`](https://github.com/microsoft/STL/blob/main/stl/inc/thread#L163): |
| 12 | + |
| 13 | +```cpp |
| 14 | +private: |
| 15 | + _Thrd_t _Thr; |
| 16 | +``` |
| 17 | + |
| 18 | +[`_Thrd_t`](https://github.com/microsoft/STL/blob/main/stl/inc/xthreads.h#L22-L26) 是一个结构体,它保有两个数据成员: |
| 19 | + |
| 20 | +```cpp |
| 21 | +using _Thrd_id_t = unsigned int; |
| 22 | +struct _Thrd_t { // thread identifier for Win32 |
| 23 | + void* _Hnd; // Win32 HANDLE |
| 24 | + _Thrd_id_t _Id; |
| 25 | +}; |
| 26 | +``` |
| 27 | +
|
| 28 | +结构很明确,这个结构体的 `_Hnd` 成员是指向线程的句柄,`_Id` 成员就是保有线程的 ID。 |
| 29 | +
|
| 30 | +在64 位操作系统,因为内存对齐,指针 8 ,无符号 int 4,这个结构体 `_Thrd_t` 就是占据 16 个字节。也就是说 `sizeof(std::thread)` 的结果应该为 **16**。 |
| 31 | +
|
| 32 | +## `std::thread` 的构造函数 |
| 33 | +
|
| 34 | +`std::thread` 有四个[构造函数](https://zh.cppreference.com/w/cpp/thread/thread/thread),分别是: |
| 35 | +
|
| 36 | +1. 默认构造函数,构造不关联线程的新 std::thread 对象。 |
| 37 | +
|
| 38 | + ```cpp |
| 39 | + thread() noexcept : _Thr{} {} |
| 40 | + ``` |
| 41 | + |
| 42 | + [值初始化](https://zh.cppreference.com/w/cpp/language/value_initialization#:~:text=%E5%87%BD%E6%95%B0%E7%9A%84%E7%B1%BB%EF%BC%89%EF%BC%8C-,%E9%82%A3%E4%B9%88%E9%9B%B6%E5%88%9D%E5%A7%8B%E5%8C%96%E5%AF%B9%E8%B1%A1,-%EF%BC%8C%E7%84%B6%E5%90%8E%E5%A6%82%E6%9E%9C%E5%AE%83)了数据成员 _Thr ,这里的效果相当于给其成员 `_Hnd` 和 `_Id` 都进行[零初始化](https://zh.cppreference.com/w/cpp/language/zero_initialization)。 |
| 43 | + |
| 44 | +2. 移动构造函数,转移线程的所有权,构造 other 关联的执行线程的 `std::thread` 对象。此调用后 other 不再表示执行线程失去了线程的所有权。 |
| 45 | + |
| 46 | + ```cpp |
| 47 | + thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {} |
| 48 | + ``` |
| 49 | +
|
| 50 | + [_STD](https://github.com/microsoft/STL/blob/main/stl/inc/yvals_core.h#L1951) 是一个宏,展开就是 `::std::`,也就是 [`::std::exchange`](https://zh.cppreference.com/w/cpp/utility/exchange),将 `_Other._Thr` 赋为 `{}` (也就是置空),返回 `_Other._Thr` 的旧值用以初始化当前对象的数据成员 `_Thr`。 |
| 51 | +
|
| 52 | +3. 复制构造函数被定义为弃置的,std::thread 不可复制。两个 std::thread 不可表示一个线程,std::thread 对线程资源是独占所有权。 |
| 53 | +
|
| 54 | + ```cpp |
| 55 | + thread(const thread&) = delete; |
| 56 | + ``` |
| 57 | + |
| 58 | +4. 构造新的 `std::thread` 对象并将它与执行线程关联。**表示新的执行线程开始执行**。 |
| 59 | + |
| 60 | + ```cpp |
| 61 | + template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0> |
| 62 | + _NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) { |
| 63 | + _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); |
| 64 | + } |
| 65 | + ``` |
| 66 | +
|
| 67 | +--- |
| 68 | +
|
| 69 | +前三个构造函数都没啥要特别聊的,非常简单,只有第四个构造函数较为复杂,且是我们本章重点,需要详细讲解。(*注意 MSVC 使用标准库的内容很多时候不加 **std::**,脑补一下就行*) |
| 70 | +
|
| 71 | +如你所见,这个构造函数本身并没有做什么,它只是一个可变参数成员函数模板,增加了一些 [SFINAE](https://zh.cppreference.com/w/cpp/language/sfinae) 进行约束我们传入的[可调用](https://zh.cppreference.com/w/cpp/named_req/Callable)对象的类型不能是 `std::thread`。函数体中调用了一个函数 [**`_Start`**](https://github.com/microsoft/STL/blob/main/stl/inc/thread#L72-L87),将我们构造函数的参数全部完美转发,去调用它,这个函数才是我们的重点,如下: |
| 72 | +
|
| 73 | +```cpp |
| 74 | +template <class _Fn, class... _Args> |
| 75 | +void _Start(_Fn&& _Fx, _Args&&... _Ax) { |
| 76 | + using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>; |
| 77 | + auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); |
| 78 | + constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{}); |
| 79 | +
|
| 80 | + _Thr._Hnd = |
| 81 | + reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id)); |
| 82 | +
|
| 83 | + if (_Thr._Hnd) { // ownership transferred to the thread |
| 84 | + (void) _Decay_copied.release(); |
| 85 | + } else { // failed to start thread |
| 86 | + _Thr._Id = 0; |
| 87 | + _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN); |
| 88 | + } |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +1. 它也是一个可变参数成员函数模板,接受一个可调用对象 `_Fn` 和一系列参数 `_Args...` ,这些东西用来创建一个线程。 |
| 93 | + |
| 94 | +2. `using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>` |
| 95 | + |
| 96 | + - 定义了一个[元组](https://zh.cppreference.com/w/cpp/utility/tuple)类型 `_Tuple` ,它包含了可调用对象和参数的类型,这里使用了 [`decay_t`](https://zh.cppreference.com/w/cpp/types/decay) 来去除了类型的引用和 cv 限定。 |
| 97 | + |
| 98 | +3. `auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...)` |
| 99 | + |
| 100 | + - 使用 [`make_unique`](https://zh.cppreference.com/w/cpp/memory/unique_ptr/make_unique) 创建了一个独占指针,指向的是 `_Tuple` 类型的对象,存储了传入的函数对象和参数的副本。 |
| 101 | + |
| 102 | +4. `constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{})` |
| 103 | + |
| 104 | + - 调用 [`_Get_invoke`](https://github.com/microsoft/STL/blob/main/stl/inc/thread#L65-L68) 函数,传入 `_Tuple` 类型和一个参数序列的索引序列(为了遍历形参包)。这个函数用于获取一个函数指针,指向了一个静态成员函数 [`_Invoke`](https://github.com/microsoft/STL/blob/main/stl/inc/thread#L55-L63),用来实际执行线程。这两个函数都非常的简单,我们来看看: |
| 105 | + |
| 106 | + ```cpp |
| 107 | + template <class _Tuple, size_t... _Indices> |
| 108 | + _NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept { |
| 109 | + return &_Invoke<_Tuple, _Indices...>; |
| 110 | + } |
| 111 | + |
| 112 | + template <class _Tuple, size_t... _Indices> |
| 113 | + static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ { |
| 114 | + // adapt invoke of user's callable object to _beginthreadex's thread procedure |
| 115 | + const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals)); |
| 116 | + _Tuple& _Tup = *_FnVals.get(); // avoid ADL, handle incomplete types |
| 117 | + _STD invoke(_STD move(_STD get<_Indices>(_Tup))...); |
| 118 | + _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI |
| 119 | + return 0; |
| 120 | + } |
| 121 | + ``` |
| 122 | + |
| 123 | + _Get_invoke 函数很简单,就是接受一个元组类型,和形参包的索引,传递给 _Invoke 静态成员函数模板,实例化,获取它的函数指针。 |
| 124 | + |
| 125 | + > 它的形参类型我们不再过多介绍,你只需要知道 [`index_sequence`](https://en.cppreference.com/w/cpp/utility/integer_sequence) 这个东西可以用来接收一个由 `make_index_sequence` 创建的索引形参包,帮助我们进行遍历即可。 |
| 126 | +
|
| 127 | + **_Invoke 是重中之重,它是线程实际执行的函数**,如你所见它的形参类型是 `void*` ,这是必须的,要符合 `_beginthreadex` 执行函数的类型要求。虽然是 `void*`,但是我可以将它转换为 `_Tuple*` 类型,构造一个独占智能指针,然后用 get 成员函数获取底层指针,解引用,获取引用,`_Tup` 接取。 |
| 128 | + |
| 129 | + 此时,我们就可以进行调用了,使用 [`std::invoke`](https://zh.cppreference.com/w/cpp/utility/functional/invoke) + `std::move`(默认移动) ,这里有一个形参包展开,`_STD get<_Indices>(_Tup))...`,_Tup 就是 std::tuple 的引用,我们使用 `std::get<>` 获取元组存储的数据,需要传入一个索引,这里就用到了 `_Indices`。展开之后,就等于 invoke 就接受了我们构造 std::thread 传入的可调用对象,调用可调用对象的参数,invoke 就可以执行了。 |
| 130 | + |
| 131 | +5. `_Thr._Hnd = reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id))` |
| 132 | + |
| 133 | + - 调用 [`_beginthreadex`](https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-170) 函数来启动一个线程,并将线程句柄存储到 `_Thr._Hnd` 中。传递给线程的参数为 `_Invoker_proc`(一个静态函数指针,就是我们前面讲的 **_Invoke**)和 `_Decay_copied.get()`(存储了函数对象和参数的副本的指针)。 |
| 134 | + |
| 135 | +6. `if (_Thr._Hnd) {` |
| 136 | + |
| 137 | + - 如果线程句柄 `_Thr._Hnd` 不为空,则表示线程已成功启动,将独占指针的所有权转移给线程。 |
| 138 | + |
| 139 | +7. `(void) _Decay_copied.release()` |
| 140 | + |
| 141 | + - 释放独占指针的所有权,因为已经将参数传递给了线程。 |
| 142 | + |
| 143 | +8. `} else { // failed to start thread` |
| 144 | + |
| 145 | + - 如果线程启动失败,则进入这个分支 |
| 146 | + |
| 147 | +9. `_Thr._Id = 0;` |
| 148 | + |
| 149 | + - 将线程ID设置为0。 |
| 150 | + |
| 151 | +10. `_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);` |
| 152 | + |
| 153 | +- 抛出一个 C++ 错误,表示资源不可用,请再次尝试。 |
| 154 | + |
| 155 | +## 总结 |
| 156 | + |
| 157 | +需要注意,libstdc++ 和 libc++ 可能不同,就比如它们 64位环境下 `sizeof(std::thread)` 的结果就是 **8**,它们的实现只保有一个线程 ID。 |
| 158 | + |
| 159 | +我们这里的源码解析涉及到的 C++ 技术很多,我们也没办法每一个都单独讲,那会显得文章很冗长,而且也不是重点。 |
| 160 | + |
| 161 | +相信你也感受到了,**不会模板,你阅读标准库源码,是无稽之谈**,市面上很多教程教学,教导一些实现容器,过度简化了,真要去出错了去看标准库的代码,那是不现实的。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:[**现代C++模板教程**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial)。 |
0 commit comments