Skip to content

Commit ad78ba2

Browse files
committed
完成 thread 的构造与源码解析
1 parent 16bb6ad commit ad78ba2

File tree

2 files changed

+161
-69
lines changed

2 files changed

+161
-69
lines changed

md/详细分析/01thread的构造.md

Lines changed: 0 additions & 69 deletions
This file was deleted.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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

Comments
 (0)