Skip to content

Commit 6db047d

Browse files
committed
更新修改“使用线程”中的部分表述,以及增加总结
1 parent 1f2d99a commit 6db047d

File tree

1 file changed

+25
-17
lines changed

1 file changed

+25
-17
lines changed

md/02使用线程.md

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 使用线程
22

3-
在标准 C++中,`std::thread` 可以指代线程。
3+
在标准 C++ 中,[`std::thread`](https://zh.cppreference.com/w/cpp/thread/thread) 可以指代线程,使用线程也就是使用 `std::thread`
44

55
## Hello World
66

@@ -16,7 +16,7 @@ int main(){
1616

1717
这段代码将"Hello World!"写入到标准输出流,换行并[刷新](https://zh.cppreference.com/w/cpp/io/manip/endl)
1818

19-
我们启动一个线程来做这件事情
19+
我们可以启动一个线程来做这件事情
2020

2121
```cpp
2222
#include <iostream>
@@ -32,9 +32,9 @@ int main(){
3232
}
3333
```
3434

35-
`std::thread t{ hello };` 创建了一个线程对象 `t`,将 `hello` 作为它的[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象,在新线程中执行。线程对象关联了一个线程资源,我们无需手动控制,在线程对象构造成功之后,就自动在新线程开始执行函数 `hello`
35+
`std::thread t{ hello };` 创建了一个线程对象 `t`,将 `hello` 作为它的[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象,在新线程中执行。线程对象关联了一个线程资源,我们无需手动控制,在线程对象构造成功,就自动在新线程开始执行函数 `hello`
3636

37-
`t.join();` 等待线程对象 `t` 关联的线程执行完毕,否则将一直堵塞。这里是必须调用的,否则 `std::thread` 的析构函数将调用 `std::terminate()`
37+
`t.join();` 等待线程对象 `t` 关联的线程执行完毕,否则将一直堵塞。这里的调用是必须的,否则 `std::thread` 的析构函数将调用 `std::terminate()` 无法正确析构
3838

3939
这是因为我们创建线程对象 `t` 的时候就关联了一个活跃的线程,调用 `join()` 就是确保线程对象关联的线程已经执行完毕,然后会修改对象的状态,让 [`std::thread::joinable()`](https://zh.cppreference.com/w/cpp/thread/thread/joinable) 返回 `false`,表示线程对象目前没有关联活跃线程。`std::thread` 的析构函数,正是通过 `joinable()` 判断线程对象目前是否有关联活跃线程,如果为 `true`,那么就当做有关联活跃线程,会调用 `std::terminate()`
4040

@@ -50,7 +50,7 @@ int main(){
5050

5151
使用 C++ 线程库启动线程,就是构造 std::thread 对象。
5252

53-
> 当然了,如果是**默认构造**,那么 `std::thread` 线程对象没有关联线程的,自然也不会启动线程执行任务。
53+
> 当然了,如果是**默认构造**之类的,那么 `std::thread` 线程对象没有关联线程的,自然也不会启动线程执行任务。
5454
>
5555
> ```cpp
5656
> std::thread t; // 构造不表示线程的新 std::thread 对象
@@ -67,14 +67,14 @@ public:
6767
};
6868
```
6969
70-
我们显然没办法直接像函数使用函数名一样,使用“类名”,函数名可以隐式转换到指向它的函数指针,我们要用使用 `Task` 就得创造对象了
70+
我们显然没办法直接像函数使用函数名一样,使用“类名”,函数名可以隐式转换到指向它的函数指针,而类名可不会直接变成对象,我们想使用 `Task` 自然就得构造对象了
7171

7272
```cpp
7373
std::thread t{ Task{} };
7474
t.join();
7575
```
7676
77-
直接创建临时对象即可,因为创建局部的也没什么别的作用
77+
直接创建临时对象即可,可以简化代码并避免引入不必要的局部对象
7878
7979
不过有件事情需要注意,当我们使用函数对象用于构造 `std::thread` 的时候,如果你传入的是一个临时对象,且使用的**都是 “`()`”小括号初始化**,那么**编译器会将此语法解析为函数声明**。
8080
@@ -110,7 +110,7 @@ std::thread t( Task (*p)() ){ return {}; } // #2 函数定义
110110

111111
所以总而言之,建议使用 `{}` 进行初始化,这是好习惯,大多数时候它是合适的。
112112

113-
C++11 引入的 Lambda 表达式,同样可以作为构造 `std::thread` 的参数,因为 Lambda 本身就是[生成](https://cppinsights.io/s/c448ad3d)了一个函数对象。
113+
C++11 引入的 Lambda 表达式,同样可以作为构造 `std::thread` 的参数,因为 Lambda 本身就是[生成](https://cppinsights.io/s/c448ad3d)了一个函数对象,它自身就是[类类型](https://zh.cppreference.com/w/cpp/language/lambda#:~:text=lambda%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%98%AF%E7%BA%AF%E5%8F%B3%E5%80%BC%E8%A1%A8%E8%BE%BE%E5%BC%8F%EF%BC%8C%E5%AE%83%E7%9A%84%E7%B1%BB%E5%9E%8B%E6%98%AF%E7%8B%AC%E6%9C%89%E7%9A%84%E6%97%A0%E5%90%8D%E9%9D%9E%E8%81%94%E5%90%88%E4%BD%93%E9%9D%9E%E8%81%9A%E5%90%88%E4%BD%93%E9%9D%9E%E7%BB%93%E6%9E%84%E5%8C%96%E7%B1%BB%E7%B1%BB%E5%9E%8B%EF%BC%8C%E8%A2%AB%E7%A7%B0%E4%B8%BA%E9%97%AD%E5%8C%85%E7%B1%BB%E5%9E%8B)
114114

115115
```cpp
116116
#include <iostream>
@@ -232,13 +232,13 @@ void f(){
232232
>
233233
> 你是否觉得这样也可以?也没问题?简单的[测试](https://godbolt.org/z/Wo7Tj95Tz)运行的确没问题。
234234
>
235-
> **但是这是不对的**,你要注意我们的注释:“**一些当前线程可能抛出异常的代码**”,而不是 `f2()`,我们的 `try` `catch` 只是为了让线程对象关联的线程得以正确执行完毕,以及线程对象正确析构。并没有处理什么其他的东西,不掩盖错误, `try` 块中的代码抛出了异常, `catch` 接住了,我们理所应当再次抛出。
235+
> **但是这是不对的**,你要注意我们的注释:“**一些当前线程可能抛出异常的代码**”,而不是 `f2()`,我们的 `try` `catch` 只是为了让线程对象关联的线程得以正确执行完毕,以及线程对象正确析构。并没有处理什么其他的东西,不掩盖错误,try` 块中的代码抛出了异常, `catch` 接住了,我们理所应当再次抛出。
236236
237237
### RAII
238238
239239
“[资源获取即初始化](https://zh.cppreference.com/w/cpp/language/raii)”(RAII,Resource Acquisition Is Initialization)。
240240
241-
简单的理解是:***构造函数申请资源,析构函数释放资源,让对象的生命周期和资源绑定***。
241+
简单的说是:***构造函数申请资源,析构函数释放资源,让对象的生命周期和资源绑定***。当异常抛出时,C++ 会自动调用栈上所有对象的析构函数
242242
243243
我们可以提供一个类,在析构函数中使用 join() 确保线程执行完成,线程对象正常析构。
244244
@@ -264,17 +264,17 @@ void f(){
264264
}
265265
```
266266
267-
函数 f 执行完毕,局部对象就要逆序销毁了。因此,thread_guard 对象 g 是第一个被销毁的,调用析构函数。**即使函数 f2() 抛出了一个异常,这个销毁依然会发生(前提是你捕获了这个异常)**。这确保了线程对象 t 所关联的线程正常的执行完毕以及线程对象的正常析构。[测试代码](https://godbolt.org/z/MaWjW73P4)
267+
函数 f 执行完毕,局部对象就要逆序销毁了。因此,thread_guard 对象 g 是第一个被销毁的,**调用析构函数****即使函数 f2() 抛出了一个异常,这个销毁依然会发生(前提是你捕获了这个异常)**。这确保了线程对象 t 所关联的线程正常的执行完毕以及线程对象的正常析构。[测试代码](https://godbolt.org/z/MaWjW73P4)
268268
269269
> 如果异常被抛出但未被捕获那么就会调用 [std::terminate](https://zh.cppreference.com/w/cpp/error/terminate)。是否对未捕获的异常进行任何栈回溯由**实现定义**。(简单的说就是不一定会调用析构)
270270
>
271271
> 我们的测试代码是捕获了异常的,为了观测,看到它一定打印“析构”。
272272
273273
在 thread_guard 的析构函数中,我们要判断 `std::thread` 线程对象现在是否有关联的活跃线程,如果有,我们才会执行 **`join()`**,阻塞当前线程直到线程对象关联的线程执行完毕。如果不想等待线程结束可以使用 `detach()` ,但是这让 `std::thread` 对象失去了线程资源的所有权,难以掌控,具体如何,看情况分析。
274274
275-
拷贝赋值和拷贝构造设置为 `=delete` 首先是防止编译器隐式生成,并且也能抑制编译器生成移动构造和移动赋值。这样的话,对 thread_guard 对象进行拷贝或赋值等操作会引发一个编译错误。
275+
拷贝赋值和拷贝构造定义为 `=delete` 可以防止编译器隐式生成,同时会[**阻止**](https://zh.cppreference.com/w/cpp/language/rule_of_three#.E4.BA.94.E4.B9.8B.E6.B3.95.E5.88.99)移动构造函数和移动赋值运算符的隐式定义。这样的话,对 thread_guard 对象进行拷贝或赋值等操作会引发一个编译错误。
276276
277-
不允许这些操作主要在于:这是个管理类,而且顾名思义,它就应该只是单纯的管理线程对象仅此而已,只保有一个引用,单纯的做好 RAII 的事情就行,允许其他操作没有价值。
277+
不允许这些操作主要在于:这是个管理类,而且顾名思义,它就应该只是单纯的管理线程对象仅此而已,只保有一个引用,**单纯的做好 RAII 的事情就行,允许其他操作没有价值。**
278278
279279
> 严格来说其实这里倒也不算 RAII,因为 thread_guard 的构造函数其实并没有申请资源,只是保有了线程对象的引用,在析构的时候进行了 join() 。
280280
@@ -351,7 +351,7 @@ int main() {
351351
352352
[运行代码](https://godbolt.org/z/hTP3ex4W7),打印地址完全相同。
353353
354-
我们来解释一下,“**ref**” 其实就是 “**reference**” (引用)的缩写,意思也很简单,返回“引用”,当然了,不是真的返回引用,它们返回一个包装类 [`std::reference_wrapper`](https://zh.cppreference.com/w/cpp/utility/functional/reference_wrapper),顾名思义,这个类就是来包装引用对象的类模板,可以隐式转换为包装对象的引用
354+
我们来解释一下,“**ref**” 其实就是 “**reference**”(引用)的缩写,意思也很简单,返回“引用”,当然了,不是真的返回引用,它们返回一个包装类 [`std::reference_wrapper`](https://zh.cppreference.com/w/cpp/utility/functional/reference_wrapper),顾名思义,这个类就是包装引用对象类模板,将对象包装,可以隐式转换为被包装对象的引用
355355
356356
“**cref**”呢?,这个“c”就是“**const**”,就是返回了 `std::reference_wrapper<const T>`。我们不详细介绍他们的实现,你简单认为`reference_wrapper`可以隐式转换为被包装对象的引用即可,
357357
@@ -551,13 +551,13 @@ void test(){
551551
}
552552
```
553553
554-
`sleep_until` 本身设置使用很简单,是打印时间格式之类的、设置时区麻烦。[运行结果](https://godbolt.org/z/4qYGbcvYW)。
554+
`sleep_until` 本身设置使用很简单,是打印时间格式、设置时区麻烦。[运行结果](https://godbolt.org/z/4qYGbcvYW)。
555555
556-
介绍了一下 `std::this_thread` 命名空间中的四个成员函数的基本用法,我们后续会经常看到这些函数的使用,不用着急。
556+
介绍了一下 `std::this_thread` 命名空间中的四个函数的基本用法,我们后续会经常看到这些函数的使用,不用着急。
557557
558558
### `std::thread` 转移所有权
559559
560-
传入可调用对象以及参数,构造 `std::thread` 对象,线程启动,而线程对象拥有了线程资源的所有权
560+
传入可调用对象以及参数,构造 `std::thread` 对象,启动线程,而线程对象拥有了线程的所有权,线程是一种系统资源,所以可称作“*线程资源*”
561561
562562
std::thread 不可复制。两个 std::thread 不可表示一个线程,std::thread 对线程资源是独占所有权。移动就是转移它的线程资源的所有权给别的 `std::thread` 对象。
563563
@@ -741,3 +741,11 @@ int main(){
741741
> [运行测试](https://godbolt.org/z/8qa95vMz4)
742742
743743
如果你自己编译了这些代码,相信你注意到了,打印的是乱序的,没什么规律,而且重复运行的结果还不一样,**这是正常现象**。多线程执行就是如此,无序且操作可能被打断。使用互斥量可以解决这些问题,这也就是下一章节的内容了。
744+
745+
## 总结
746+
747+
本章节的内容围绕着:“使用线程”,也就是"**使用 `std::thread`**"展开, `std::thread` 是我们学习 C++ 并发支持库的重中之重,本章的内容并不少见,但是却是少有的准确与完善。即使你早已学习过乃至使用 C++ 标准库进行多线程编程已经很久,我相信本章也一定可以让你收获良多。
748+
749+
如果是第一次学习,有还不够理解的地方,则一定要多思考,或记住,以后多看。
750+
751+
我尽量讲的简单与通俗易懂。学完本章,你大概率还无法在实际环境使用多线程提升程序效率,至少也要学习到使用互斥量,保护共享数据,才可实际使用。

0 commit comments

Comments
 (0)