10
10
11
11
- 在规定时间内等待
12
12
13
- 本章将讨论如何使用条件变量等待事件,介绍 future, 等标准库设施用作同步操作。
13
+ 本章将讨论如何使用条件变量等待事件,介绍 future等标准库设施用作同步操作。
14
14
15
15
## 等待事件或条件
16
16
17
17
假设你正在一辆夜间运行的地铁上,那么你要如何在正确的站点下车呢?
18
18
19
19
1 . 一直不休息,每一站都能知道,这样就不会错过你要下车的站点,但是这会很疲惫。
20
20
21
- 2 . 可以看一下时间,估算一下火车到达目的地的时间 ,然后设置一个稍早的闹钟,就休息。这个方法听起来还行,但是你可能被过早的叫醒,甚至估算错误导致坐过站,又或者闹钟没电了睡过站。
21
+ 2 . 可以看一下时间,估算一下地铁到达目的地的时间 ,然后设置一个稍早的闹钟,就休息。这个方法听起来还行,但是你可能被过早的叫醒,甚至估算错误导致坐过站,又或者闹钟没电了睡过站。
22
22
23
23
3 . 事实上最简单的方式是,到站的时候有人或者其它东西能将你叫醒(比如手机的地图,到达设置的位置就提醒)。
24
24
@@ -107,7 +107,7 @@ void simulateArrival() {
107
107
108
108
条件变量的 ` wait ` 成员函数有两个版本,以上代码使用的就是第二个版本,传入了一个[ * 谓词* ] ( https://zh.cppreference.com/w/cpp/named_req/Predicate ) 。
109
109
110
- ``` txt
110
+ ``` cpp
111
111
void wait (std::unique_lock< std::mutex > & lock); // 1
112
112
113
113
template<class Predicate >
@@ -121,7 +121,7 @@ while (!pred())
121
121
wait(lock);
122
122
```
123
123
124
- 这可以避免"" [虚假唤醒(spurious wakeup)](https://en.wikipedia.org/wiki/Spurious_wakeup)"。
124
+ 这可以避免"[ 虚假唤醒(spurious wakeup)] ( https://en.wikipedia.org/wiki/Spurious_wakeup ) "。
125
125
126
126
> 条件变量虚假唤醒是指在使用条件变量进行线程同步时,有时候线程可能会在没有收到通知的情况下被唤醒。问题取决于程序和系统的具体实现。解决方法很简单,在循环中等待并判断条件可一并解决。使用 C++ 标准库则没有这个烦恼了。
127
127
@@ -266,3 +266,155 @@ Consumer 线程弹出元素 4:
266
266
```
267
267
268
268
到此,也就可以了。
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