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
7373std::thread t{ Task{} };
7474t.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
562562std::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