Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 97f5b6b

Browse files
committed
1. 完成第四章"创建异步任务并获取返回值"这一小节
2. 修改第二章的"传递参数"的部分格式、措辞、内容、顺序 3. 修改第四章部分的不合理措辞以及格式
1 parent 846729e commit 97f5b6b

File tree

2 files changed

+185
-31
lines changed

2 files changed

+185
-31
lines changed

‎md/02使用线程.md‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -427,33 +427,7 @@ int main() {
427427
}
428428
```
429429
430-
> 这里如果不使用 `std::ref` 并不会和前面一样只是多了拷贝,而是会产生[**编译错误**](https://godbolt.org/z/xhrhs6Ke5),这是因为 `std::thread` 内部会将保有的参数副本转换为右值表达式进行传递,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以产生编译错误。
431-
>
432-
> ```cpp
433-
> struct X{
434-
> X() { std::puts("默认构造"); }
435-
> X(const X&) = delete;
436-
> X(X&&)noexcept{
437-
> std::puts("移动构造");
438-
> }
439-
> };
440-
>
441-
> void f(X){}
442-
>
443-
> int main(){
444-
> X x;
445-
> std::thread t{ f,std::move(x) };
446-
> t.join();
447-
> }
448-
> ```
449-
>
450-
> > [运行](https://godbolt.org/z/P7eP5q4ea)测试。
451-
>
452-
> 没有 `std::ref` 自然是会保有一个副本,所以有两次移动构造,一次是被 `std::thread` 构造函数中内部保有,一次是调用函数 `f`。
453-
>
454-
> 如果还有不理解,不用担心,记住,这一切的问题都会在后面的 [`std::thread` 的构造-源码解析](#stdthread-的构造-源码解析) 解释清楚。
455-
456-
[运行代码](https://godbolt.org/z/hTP3ex4W7),打印地址完全相同。
430+
> [运行代码](https://godbolt.org/z/hTP3ex4W7),打印地址完全相同。
457431
458432
我们来解释一下,"**ref**" 其实就是 "**reference**"(引用)的缩写,意思也很简单,返回"引用",当然了,不是真的返回引用,它们返回一个包装类 [`std::reference_wrapper`](https://zh.cppreference.com/w/cpp/utility/functional/reference_wrapper),顾名思义,这个类就是包装引用对象类模板,将对象包装,可以隐式转换为被包装对象的引用。
459433
@@ -475,6 +449,34 @@ const int& p = r; // r 隐式转换为 n 的 const 的引用 此时 p 引用的
475449
476450
---
477451
452+
以上代码`void f(int, int&)` 如果不使用 `std::ref` 并不会和前面 `void f(int, const int&)` 一样只是多了拷贝,而是会产生[**编译错误**](https://godbolt.org/z/xhrhs6Ke5),这是因为 `std::thread` 内部会将保有的参数副本转换为**右值表达式进行传递**,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以产生编译错误。
453+
454+
```cpp
455+
struct move_only {
456+
move_only() { std::puts("默认构造"); }
457+
move_only(const move_only&) = delete;
458+
move_only(move_only&&)noexcept {
459+
std::puts("移动构造");
460+
}
461+
};
462+
463+
void f(move_only){}
464+
465+
int main(){
466+
move_only obj;
467+
std::thread t{ f,std::move(obj) };
468+
t.join();
469+
}
470+
```
471+
472+
> [运行](https://godbolt.org/z/b6fYWaf3Y)测试。
473+
474+
没有 `std::ref` 自然是会保有一个副本,所以有两次移动构造,一次是被 `std::thread` 构造函数中初始化副本,一次是调用函数 `f`。
475+
476+
如果还有不理解,不用担心,记住,这一切的问题都会在后面的 [`std::thread` 的构造-源码解析](#stdthread-的构造-源码解析) 解释清楚。
477+
478+
---
479+
478480
[**成员函数指针**](https://zh.cppreference.com/w/cpp/language/pointer#.E6.88.90.E5.91.98.E5.87.BD.E6.95.B0.E6.8C.87.E9.92.88)也是[*可调用*](https://zh.cppreference.com/w/cpp/named_req/Callable)(*Callable*)的 ,可以传递给 `std::thread` 作为构造参数,让其关联的线程执行成员函数。
479481
480482
```cpp

‎md/04同步操作.md‎

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010

1111
- 在规定时间内等待
1212

13-
本章将讨论如何使用条件变量等待事件,介绍 future,等标准库设施用作同步操作。
13+
本章将讨论如何使用条件变量等待事件,介绍 future等标准库设施用作同步操作。
1414

1515
## 等待事件或条件
1616

1717
假设你正在一辆夜间运行的地铁上,那么你要如何在正确的站点下车呢?
1818

1919
1. 一直不休息,每一站都能知道,这样就不会错过你要下车的站点,但是这会很疲惫。
2020

21-
2. 可以看一下时间,估算一下火车到达目的地的时间,然后设置一个稍早的闹钟,就休息。这个方法听起来还行,但是你可能被过早的叫醒,甚至估算错误导致坐过站,又或者闹钟没电了睡过站。
21+
2. 可以看一下时间,估算一下地铁到达目的地的时间,然后设置一个稍早的闹钟,就休息。这个方法听起来还行,但是你可能被过早的叫醒,甚至估算错误导致坐过站,又或者闹钟没电了睡过站。
2222

2323
3. 事实上最简单的方式是,到站的时候有人或者其它东西能将你叫醒(比如手机的地图,到达设置的位置就提醒)。
2424

@@ -107,7 +107,7 @@ void simulateArrival() {
107107

108108
条件变量的 `wait` 成员函数有两个版本,以上代码使用的就是第二个版本,传入了一个[*谓词*](https://zh.cppreference.com/w/cpp/named_req/Predicate)
109109

110-
```txt
110+
```cpp
111111
void wait(std::unique_lock<std::mutex>& lock); // 1
112112

113113
template<class Predicate>
@@ -121,7 +121,7 @@ while (!pred())
121121
wait(lock);
122122
```
123123

124-
这可以避免""[虚假唤醒(spurious wakeup)](https://en.wikipedia.org/wiki/Spurious_wakeup)"。
124+
这可以避免"[虚假唤醒(spurious wakeup)](https://en.wikipedia.org/wiki/Spurious_wakeup)"。
125125

126126
> 条件变量虚假唤醒是指在使用条件变量进行线程同步时,有时候线程可能会在没有收到通知的情况下被唤醒。问题取决于程序和系统的具体实现。解决方法很简单,在循环中等待并判断条件可一并解决。使用 C++ 标准库则没有这个烦恼了。
127127
@@ -266,3 +266,155 @@ Consumer 线程弹出元素 4:
266266
```
267267

268268
到此,也就可以了。
269+
270+
## 使用 `future`
271+
272+
其实就是**异步**
273+
274+
举个例子:我们在车站等车,你可能会做一些别的事情打发时间,比如学习[现代 C++ 模板教程](https://github.com/Mq-b/Modern-Cpp-templates-tutorial)、观看 [mq白](https://space.bilibili.com/1292761396) 的视频教程、玩手机等。不过,你始终在等待一件事情:***车到站***
275+
276+
C++ 标准库将这种事件称为 [future](https://zh.cppreference.com/w/cpp/thread#.E6.9C.AA.E6.9D.A5.E4.BD.93)。它用于处理线程中需要等待某个事件的情况,线程知道预期结果。等待的同时也可以执行其它的任务。
277+
278+
C++ 标准库有两种 future,都声明在 [`<future>`](https://zh.cppreference.com/w/cpp/header/future) 头文件中:独占的 [`std::future`](https://zh.cppreference.com/w/cpp/thread/future) 、共享的 [`std::shared_future`](https://zh.cppreference.com/w/cpp/thread/shared_future)。它们的区别与 `std::unique_ptr``std::shared_ptr` 类似。`std::future` 只能与**单个**指定事件关联,而 `std::shared_future` 能关联**多个**事件。它们都是模板,它们的模板类型参数,就是其关联的事件(函数)的返回类型。当多个线程需要访问一个独立 future 对象时, 必须使用互斥量或类似同步机制进行保护。而多个线程访问同一共享状态,若每个线程都是通过其自身的 `shared_future` 对象副本进行访问,则是安全的。
279+
280+
最简单的作用是,我们先前讲的 `std::thread` 执行任务是没有返回值的,这个问题就能使用 future 解决。
281+
282+
### 创建异步任务获取返回值
283+
284+
假设需要执行一个耗时任务并获取其返回值,但是并不急切的需要它。那么久可以启动新线程计算,然而 `std::thread` 没提供直接接收返回值的机制。所以我们可以使用 [`std::async`](https://zh.cppreference.com/w/cpp/thread/async) 函数模板。
285+
286+
使用 `std::async` 启动一个异步任务,它会返回一个 `std::future` 对象,这个对象和任务关联,将持有最终计算出来的结果。当需要这个值的时候,只需要调用 [`get()`](https://zh.cppreference.com/w/cpp/thread/future/get) 成员函数,就会阻塞直到 `future` 为就绪为止,返回执行结果。
287+
288+
```cpp
289+
#include <iostream>
290+
#include <thread>
291+
#include <future>
292+
293+
int task(int n){
294+
std::cout << "异步任务 ID: " << std::this_thread::get_id << '\n';
295+
return n * n;
296+
}
297+
298+
int main(){
299+
std::future<int> future = std::async(task, 10);
300+
std::cout << "main\n";
301+
std::cout << future.get() << '\n';
302+
}
303+
```
304+
305+
> [运行](https://godbolt.org/z/5xvT1x86c)测试。
306+
307+
与 `std::thread` 一样,`std::async` 支持任意[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象,以及传递调用参数。包括支持使用 `std::ref` ,以及移动的问题。我们下面详细聊一下 `std::async` 参数传递的事。
308+
309+
```cpp
310+
struct X{
311+
int operator()(int n)const{
312+
return n * n;
313+
}
314+
};
315+
struct Y{
316+
int f(int n)const{
317+
return n * n;
318+
}
319+
};
320+
void f(int& p) { std::cout << &p << '\n'; }
321+
322+
int main(){
323+
Y y;
324+
int n = 0;
325+
auto t1 = std::async(X{}, 10);
326+
auto t2 = std::async(&Y::f,&y,10);
327+
auto t3 = std::async([] {});
328+
auto t4 = std::async(f, std::ref(n));
329+
std::cout << &n << '\n';
330+
}
331+
```
332+
333+
> [运行](https://godbolt.org/z/fEvs3M3vv)测试。
334+
335+
如你所见,它支持所有[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象,并且也是默认拷贝,必须使用 `std::ref` 才能传递引用。并且它和 `std::thread` 一样,内部会将保有的参数副本转换为**右值表达式进行传递**,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以如果不使用 `std::ref`,这里 `void f(int&)` 就会导致编译错误,如果是 `void f(const int&)` 则可以通过编译,不过引用的不是我们传递的局部对象。
336+
337+
```cpp
338+
void f(const int& p) {}
339+
void f2(int& p ){}
340+
341+
int n = 0;
342+
std::async(f, n); // OK! 可以通过编译,不过引用的并非是局部的n
343+
std::async(f2, n); // Error! 无法通过编译
344+
```
345+
346+
我们来展示使用 `std::move` ,也就移动传递参数:
347+
348+
```cpp
349+
struct move_only {
350+
move_only() { std::puts("默认构造"); }
351+
move_only(const move_only&) = delete;
352+
move_only(move_only&&)noexcept {
353+
std::puts("移动构造");
354+
}
355+
};
356+
357+
void task(move_only x){
358+
std::cout << "异步任务 ID: " << std::this_thread::get_id() << '\n';
359+
}
360+
361+
int main(){
362+
move_only x;
363+
std::future<void> future = std::async(task, std::move(x));
364+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
365+
std::cout << "main\n";
366+
future.wait(); // 等待异步任务执行完毕
367+
}
368+
```
369+
370+
> [运行](https://godbolt.org/z/fY9Md3nzz)测试。
371+
372+
如你所见,它支持只移动类型,我们将参数使用 `std::move` 传递。
373+
374+
---
375+
376+
接下来我们聊 `std::async` 的执行策略,我们前面一直没有使用,其实就是在传递可调用对象与参数之前传递枚举值罢了:
377+
378+
1. `std::launch::async` 在不同线程上执行异步任务。
379+
2. `std::launch::deferred` 惰性求值,不创建线程,等待 `future` 对象调用 `wait``get` 成员函数的时候执行任务。
380+
381+
而我们先前没有写明这个参数,实际上是**默认**:`std::launch::async | std::launch::deferred` ,也就是说由实现选择到底是否创建线程执行异步任务。我们来展示一下:
382+
383+
```cpp
384+
void f(){
385+
std::cout << std::this_thread::get_id() << '\n';
386+
}
387+
388+
int main(){
389+
std::cout << std::this_thread::get_id() << '\n';
390+
auto f1 = std::async(std::launch::deferred, f);
391+
f1.wait(); // 在 wait 或 get() 调用时执行,不创建线程
392+
auto f2 = std::async(std::launch::async,f); // 创建线程执行异步任务
393+
auto f3 = std::async(std::launch::deferred | std::launch::async, f); // 实现选择的执行方式
394+
}
395+
```
396+
397+
> [运行](https://godbolt.org/z/abr96xqvM)测试。
398+
399+
---
400+
401+
其实到此基本就差不多了,我们再介绍两个常见问题即可:
402+
403+
1. 如果从 `std::async` 获得的 [std::future](https://zh.cppreference.com/w/cpp/thread/future) 没有被移动或绑定到引用,那么在完整表达式结尾, [std::future](https://zh.cppreference.com/w/cpp/thread/future)**析构函数将阻塞到异步计算完成**
404+
405+
```cpp
406+
std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
407+
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始
408+
```
409+
410+
如你所见,这并不能创建异步任务,会堵塞,然后逐个执行。
411+
412+
2. 被移动的 `std::future` 没有所有权,失去共享状态,不能调用 `get`、`wait` 成员函数。
413+
414+
```cpp
415+
auto t = std::async([] {});
416+
std::future<void> future{ std::move(t) };
417+
t.wait(); // Error! 抛出异常
418+
```
419+
420+
如同没有线程资源所有权的 `std::thread` 对象调用 `join()` 一样错误,这是移动语义的基本语义逻辑。

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /