Skip to content

Commit 3013934

Browse files
committed
完成 std::threadRAII 的内容,修改一些“基本概念”
1 parent 7b4abd1 commit 3013934

File tree

2 files changed

+39
-9
lines changed

2 files changed

+39
-9
lines changed

md/01基本概念.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020

2121
2. 单核机器的**任务切换**
2222

23-
  在早期,一些单核机器,它要想并发,执行多个任务,那就只能是任务切换,任务切换会给你一种“好像这些任务都在同时执行的假象”。只有硬件上是多核的,才能进行真正的并行,也就是真正的”**同时执行任务**“。
23+
  在早期,一些单核机器,它要想并发,执行多个任务,那就只能是任务切换,任务切换会给你一种“**好像**这些任务都在同时执行”的假象。只有硬件上是多核的,才能进行真正的并行,也就是真正的”**同时执行任务**“。
2424

2525
  在现在,我们日常使用的机器,基本上是二者都有。我们现在的 CPU 基本都是多核,而操作系统调度基本也一样有任务切换,因为要执行的任务非常之多,CPU 是很快的,但是核心却没有那么多,不可能每一个任务都单独给一个核心。大家可以打开自己电脑的任务管理器看一眼,进程至少上百个,线程更是上千。这基本不可能每一个任务分配一个核心,都并行,而且也没必要。正是任务切换使得这些后台任务可以运行,这样系统使用者就可以同时运行文字处理器、编译器、编辑器和 Web 浏览器。
2626

2727
## 并发与并行
2828

29-
事实上,对于这两个术语,并没有非常明确和公认的说法
29+
事实上,对于这两个术语,并没有非常公认的说法
3030

3131
1. 有些人认为二者毫无关系,指代的东西完全不同。
3232

md/02使用thread.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ int main(){
5151
> 当然了,如果是**默认构造**,那么 `std::thread` 线程对象没有关联线程的,自然也不会启动线程执行任务。
5252
>
5353
> ```cpp
54-
> std::thread t; // 构造不表示线程的新 std::thread 对象-
54+
> std::thread t; // 构造不表示线程的新 std::thread 对象
5555
> ```
5656
5757
我们上一节的示例是传递了一个函数给 `std::thread` 对象,函数会在新线程中执行。`std::thread` 支持的形式还有很多,只要是[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象即可,比如重载了 `operator()` 的类对象(也可以直接叫函数对象)。
@@ -136,11 +136,11 @@ int main(){
136136
#include <iostream>
137137
#include <thread>
138138

139-
struct func{
139+
struct func {
140140
int& m_i;
141141
func(int& i) :m_i{ i } {}
142-
void operator()(int n){
143-
for(int i=0;i<=n;++i){
142+
void operator()(int n)const {
143+
for (int i = 0; i <= n; ++i) {
144144
m_i += i; // 可能悬空引用
145145
}
146146
}
@@ -161,7 +161,7 @@ int main(){
161161
162162
解决方法很简单,将 detach() 替换为 join()。
163163
164-
>**通常非常不推荐使用 detach(),因为程序员必须确保所有创建的线程正常退出,释放所有获取的资源并执行其他必要的清理操作。这意味着通过调用 detach() 放弃线程的所有权不是一种选择,因此 join 应该在所有场景中使用。** 一些老式特殊情况不聊。
164+
>**通常非常不推荐使用 detach(),因为程序员必须确保所有创建的线程正常退出,释放所有获取的资源并执行其它必要的清理操作。这意味着通过调用 detach() 放弃线程的所有权不是一种选择,因此 join 应该在所有场景中使用。** 一些老式特殊情况不聊。
165165
166166
另外提示一下,也**不要想着** detach() 之后,再次调用 join()
167167
@@ -207,7 +207,7 @@ void f(){
207207

208208
我知道你可能有很多疑问,我们既然 catch 接住了异常,为什么还要 throw?以及为什么我们要两个 join()?
209209

210-
这两个问题其实也算一个问题,如果代码里抛出了异常,就会跳转到 catch 的代码中,执行 join() 确保线程正常执行完成,线程对象可以正常析构。然而此时我们必须再次 throw 抛出异常,因为你要是不抛出,那么你不是还得执行一个 `t.join()`?显然逻辑不对,自然抛出。至于这个**函数产生的异常,由调用方进行处理**,我们只是确保函数 f 中创建的线程正常执行完成,其局部对象正常析构释放。[测试代码](https://godbolt.org/z/33ajh893P)
210+
这两个问题其实也算一个问题,如果代码里抛出了异常,就会跳转到 catch 的代码中,执行 join() 确保线程正常执行完成,线程对象可以正常析构。然而此时我们必须再次 throw 抛出异常,因为你要是不抛出,那么你不是还得执行一个 `t.join()`?显然逻辑不对,自然抛出。至于这个**函数产生的异常,由调用方进行处理**,我们只是确保函数 f 中创建的线程正常执行完成,其局部对象正常析构释放。[测试代码](https://godbolt.org/z/jo5sPvPGE)
211211

212212
### RAII
213213

@@ -218,5 +218,35 @@ void f(){
218218
我们可以提供一个类,在析构函数中使用 join() 确保线程执行完成,线程对象正常析构。
219219

220220
```cpp
221-
221+
class thread_guard{
222+
std::thread& m_t;
223+
public:
224+
explicit thread_guard(std::thread& t) :m_t{ t } {}
225+
~thread_guard(){
226+
std::puts("析构"); // 打印 不用在乎
227+
if (m_t.joinable()) { // 没有关联活跃线程
228+
m_t.join();
229+
}
230+
}
231+
thread_guard(const thread_guard&) = delete;
232+
thread_guard& operator=(const thread_guard&) = delete;
233+
};
234+
void f(){
235+
int n = 0;
236+
std::thread t{ func{n},10 };
237+
thread_guard g(t);
238+
f2(); // 可能抛出异常
239+
}
222240
```
241+
242+
函数 f 执行完毕,局部对象就要逆序销毁了。因此,thread_guard 对象 g 是第一个被销毁的,调用析构函数。**即使函数 f2() 抛出了一个异常,这个销毁依然会发生(前提是你处理了这个异常)**。这确保了线程对象 t 所关联的线程正常的执行完毕以及线程对象的正常析构。[测试代码](https://godbolt.org/z/MaWjW73P4)
243+
244+
在 thread_guard 的析构函数中,我们要判断 `std::thread` 线程对象现在是否有关联的活跃线程,如果有,我们才会执行 **`join()`**,阻塞当前线程直到线程对象关联的线程执行完毕。如果不想等待线程结束可以使用 `detach()` ,但是这让 `std::thread` 对象失去了线程资源的所有权,难以掌控,具体如何,看情况分析。
245+
246+
拷贝赋值和拷贝构造设置为 `=delete` 首先是防止编译器隐式生成,并且也能抑制编译器生成移动构造和移动赋值。这样的话,对 thread_guard 对象进行拷贝或赋值等操作会引发一个编译错误。
247+
248+
不允许这些操作主要在于:这是个管理类,而且顾名思义,它就应该只是单纯的管理线程对象仅此而已,只保有一个引用,单纯的做好 RAII 的事情就行,允许其他操作没有价值。
249+
250+
> 其实这里倒也不算非常符合 RAII,因为 thread_guard 的构造函数其实并没有申请资源只是保有了线程对象的引用,在析构的时候进行了 join() 。
251+
252+
### 传递参数

0 commit comments

Comments
 (0)