Update item39.md

This commit is contained in:
猫耳堀川雷鼓 2021-03-12 22:40:43 +08:00 committed by GitHub
parent b5f0476254
commit 8609ff85d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,209 +1,206 @@
## Item 39: Consider void futures for one-shot event communication ## 条款三十九:对于一次性事件通信考虑使用`void`的*futures*
## Item 39:对于一次性事件通信考虑使用无返回futures **Item 39: Consider `void` futures for one-shot event communication**
有时,一个任务通知另一个异步执行的任务发生了特定的事件很有用,因为第二个任务要等到特定事件发生之后才能继续执行。事件也许是数据已经初始化,也许是计算阶段已经完成,或者检测到重要的传感器值。这种情况,什么是线程间通信的最佳方案? 有时,一个任务通知另一个异步执行的任务发生了特定的事件很有用,因为第二个任务要等到这个事件发生之后才能继续执行。事件也许是一个数据结构已经初始化,也许是计算阶段已经完成,或者检测到重要的传感器值。这种情况,线程间通信的最佳方案是什么
一个明显的方案就是使用条件变量condvar。如果我们将检测条件的任务称为检测任务对条件作出反应的任务称为反应任务策略很简单反应任务等待一个条件变量检测任务在事件发生时改变条件变量。代码如下 一个明显的方案就是使用条件变量(*condition variable*,简称*condvar*)。如果我们将检测条件的任务称为**检测任务***detecting task*,对条件作出反应的任务称为**反应任务***reacting task*,策略很简单:反应任务等待一个条件变量,检测任务在事件发生时改变条件变量。代码如下:
```cpp ```cpp
std::condition_variable cv; // condvar for event std::condition_variable cv; //事件的条件变量
std::mutex m; // mutex for use with cv std::mutex m; //配合cv使用的mutex
``` ```
检测任务中的代码不能再简单了: 检测任务中的代码不能再简单了:
```cpp ```cpp
... // detect event … //检测事件
cv.notify_one(); // tell reacting task cv.notify_one(); //通知反应任务
``` ```
如果有多个反应任务需要被通知,使用`notify_all()代替notify_one()`,但是这里,我们假定只有一个反应任务需要通知。 如果有多个反应任务需要被通知,使用`notify_all`代替`notify_one`,但是这里,我们假定只有一个反应任务需要通知。
反应任务对的代码稍微复杂一点,因为在调用`wait`条件变量之前,必须通过`std::unique_lock`对象使用互斥锁`mutex`来同步lock a mutex是等待条件变量的经典实现。`std::unique_lock`是C++11的易用API代码如下: 反应任务的代码稍微复杂一点,因为在对条件变量调用`wait`之前,必须通过`std::unique_lock`对象锁住互斥锁。(在等待条件变量前锁住互斥锁对线程库来说是经典操作。通过`std::unique_lock`锁住互斥锁的需要仅仅是C++11 API要求的一部分。概念上的代码如下:
```cpp ```cpp
... // propare to react … //反应的准备工作
{ // open critical section { //开启关键部分
std::unique_lock<std::mutex> lk(m); // lock mutex
cv.wait(lk); // wati for notify; this isn't correct! std::unique_lock<std::mutex> lk(m); //锁住互斥锁
... // react to event(m is blocked)
} // close crit. section; unlock m via lk's dtor cv.wait(lk); //等待通知,但是这是错的!
... // continue reacting (m now unblocked)
… //对事件进行反应m已经上锁
} //关闭关键部分通过lk的析构函数解锁m
… //继续反应动作m现在未上锁
``` ```
这份代码的第一个问题就是有时被称为*code smell*:即使代码正常工作,但是有些事情也不是很正确。这种问题源自于使用互斥锁。互斥锁被用于保护共享数据的访问,但是可能检测任务和反应任务不会同时访问共享数据,比如说,检测任务会初始化一个全局数据结构,然后给反应任务用,如果检测任务在初始化之后不会再访问这个数据,而反应任务在初始化之前不会访问这个数据,就不存在数据竞争,也就没有必要使用互斥锁。但是条件变量必须使用互斥锁,这就留下了令人不适的设计。 这份代码的第一个问题就是有时被称为**代码异味***code smell*)的一种情况:即使代码正常工作,但是有些事情也不是很正确。在这个情况中,这种问题源自于使用互斥锁。互斥锁被用于保护共享数据的访问,但是可能检测任务和反应任务可能不会同时访问共享数据,比如说,检测任务会初始化一个全局数据结构,然后给反应任务用,如果检测任务在初始化之后不会再访问这个数据结构,而在检测任务表明数据结构准备完了之前反应任务不会访问这个数据结构,这两个任务在程序逻辑下互不干扰,也就没有必要使用互斥锁。但是条件变量的方法必须使用互斥锁,这就留下了令人不适的设计。
即使你忽略这个问题,还有两个问题需要注意: 即使你忽略这个问题,还有两个问题需要注意:
- **如果检测任务在反应任务`wait`之前通知条件变量,反应任务会挂起**。为了能使条件变量唤醒另一个任务,任务必须等待在条件变量上。如果检测任务在反应任务`wait`之前就通知了条件变量,反应任务就会丢失这次通知,永远不被唤醒 - **如果在反应任务`wait`之前检测任务通知条件变量,反应任务会挂起**。为了能使条件变量唤醒另一个任务,任务必须等待在条件变量上。如果在反应任务`wait`之前检测任务就通知了条件变量,反应任务就会丢失这次通知,永远不被唤醒
- **`wait`语句虚假唤醒**。线程API的存在一个事实不只是C++)即使条件变量没有被通知,也可能被虚假唤醒,这种唤醒被称为*spurious wakeups*。正确的代码通过确认条件变量进行处理,并将其作为唤醒后的第一个操作。C++条件变量的API使得这种问题很容易解决因为允许lambda或者其他函数对象来测试等待条件。因此可以将反应任务这样写: - **`wait`语句虚假唤醒**。线程API的存在一个事实在许多语言中——不只是C++),等待一个条件变量的代码即使在条件变量没有被通知时,也可能被唤醒,这种唤醒被称为**虚假唤醒***spurious wakeups*)。正确的代码通过确认要等待的条件确实已经发生来处理这种情况,并将这个操作作为唤醒后的第一个操作。C++条件变量的API使得这种问题很容易解决因为允许把一个测试要等待的条件的*lambda*(或者其他函数对象)传给`wait`。因此,可以将反应任务`wait`调用这样写:
```cpp ```cpp
cv.wait(lk, cv.wait(lk,
[] { return whether the evet has occurred; }); []{ return whether the evet has occurred; });
``` ```
要利用这个能力需要反应任务能够确定其等待的条件为真。但是我们考虑的情况下,它正在等待的条件是检测线程负责识别的事件。反应线程可能无法确定等待的事件是否已发生。这就是为什么需要一个条件变量的原因 要利用这个能力需要反应任务能够确定其等待的条件是否为真。但是我们考虑的场景中,它正在等待的条件是检测线程负责识别的事件的发生情况。反应线程可能无法确定等待的事件是否已发生。这就是为什么它在等待一个条件变量!
在很多情况下,使用条件变量进行任务通信非常合适,但是也有不那么合适的情况。 在很多情况下,使用条件变量进行任务通信非常合适,但是也有不那么合适的情况。
对于很多开发者来说,他们的下一个诀窍是共享的boolean标志。flag被初始化为false。当检测线程识别到发生的事件将flag设置为true 对于很多开发者来说,他们的下一个诀窍是共享的布尔型flag。flag被初始化为`false`。当检测线程识别到发生的事件将flag置位
```cpp ```cpp
std::atomic<bool> flag(false); // shared flag; see Item 40 for std::atomic std::atomic<bool> flag(false); //共享的flagstd::atomic见条款40
... // detect event … //检测某个事件
flag = true; // tell reacting task flag = true; //告诉反应线程
``` ```
就其本身而言反应线程轮询该flag。当发现flag被设置为true,它就知道等待的事件已经发生了: 就其本身而言反应线程轮询该flag。当发现flag被置位,它就知道等待的事件已经发生了:
```cpp ```cpp
... // prepare … //准备作出反应
while(!flag); // wait for event while (!flag); //等待事件
... // react to event … //对事件作出反应
``` ```
这种方法不存在基于条件变量的缺点。不需要互斥锁在反应变量设置flag为true之前轮询不会出现问题,并且不会出现虚假唤醒。好,好,好。 这种方法不存在基于条件变量的设计的缺点。不需要互斥锁在反应任务开始轮询之前检测任务就对flag置位也不会出现问题,并且不会出现虚假唤醒。好,好,好。
不好的一点是反应任务中轮询的开销。在等待flag为设置为true的时候任务基本被锁住了但是一直占用cpu。这样反应线程占用了可能给另一个任务使用的硬件线程每次启动或者完成的时间片都会产生上下文切换的开销并且保持CPU核心运行否则可能会停下来省电。一个真正blocked的任务不会这样这也是基于条件变量的优点因为等待调用中的任务真的blocked 不好的一点是反应任务中轮询的开销。在任务等待flag被置位的时间里任务基本被阻塞了但是一直在运行。这样反应线程占用了可能能给另一个任务使用的硬件线程每次启动或者完成它的时间片都增加了上下文切换的开销并且保持核心一直在运行状态否则的话本来可以停下来省电。一个真正阻塞的任务不会发生上面的任何情况。这也是基于条件变量的优点因为`wait`调用中的任务真的阻塞住了
将条件变量和flag的设计组合起来很常用。一个flag表示是否发生了感兴趣的事件但是通过互斥锁同步了对该flag的访问。因为互斥锁阻止并发该flag所以如Item 40所述不需要将flag设置为`std::atomic`。一个简单的bool类型就可以检测任务代码如下 将条件变量和flag的设计组合起来很常用。一个flag表示是否发生了感兴趣的事件但是通过互斥锁同步了对该flag的访问。因为互斥锁阻止并发访问该flag所以如[Item40](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md)所述不需要将flag设置为`std::atomic`。一个简单的`bool`类型就可以,检测任务代码如下:
```cpp ```cpp
std::conditon_variable cv; std::condition_variable cv; //跟之前一样
std::mutex m; std::mutex m;
bool flag(false); // not std::atomic bool flag(false); //不是std::atomic
... // detect event … //检测某个事件
{ {
std::lock_guard<std::mutex> g(m); // lock m via g's ctor std::lock_guard<std::mutex> g(m); //通过g的构造函数锁住m
flag = true; // tell reacting task(part 1) flag = true; //通知反应任务第1部分
} // unlock m via g's dtor } //通过g的析构函数解锁m
cv.notify_one(); // tell reacting task (part 2) cv.notify_one(); //通知反应任务第2部分
``` ```
反应任务代码如下: 反应任务代码如下:
```cpp ```cpp
... // prepare to react … //准备作出反应
{ { //跟之前一样
std::unique_lock<std::mutex> lk(m); std::unique_lock<std::mutex> lk(m); //跟之前一样
cv.wait(lk, []{ return flag; }); // use lambda to avoid spurious wakeups cv.wait(lk, [] { return flag; }); //使用lambda来避免虚假唤醒
... // react to event (m is blocked) … //对事件作出反应m被锁定
} }
... // continue reacting (m now unblocked) … //继续反应动作m现在解锁
``` ```
这份代码解决了我们一直讨论的问题。无论是否反应线程调用`wait`之前还是之后检测线程对条件变量发出通知都可以工作即使出现了虚假唤醒也可以工作而且不需要轮询。但是仍然有些古怪因为检测任务通过奇怪的方式与反应线程通信。译者注下面的话挺绕的可以参考原文检测任务通知条件变量告诉反应线程等待的事件可能发生了反应线程必须通过检查flag来确保事件发生了。检测线程置flag来告诉反应线程事件确实发生了但是检测线程首先需要通知条件变量唤醒反应线程来检查flag。这种方案是可以工作的但是不太优雅。 这份代码解决了我们一直讨论的问题。无论在检测线程对条件变量发出通知之前反应线程是否调用了`wait`都可以工作,即使出现了虚假唤醒也可以工作,而且不需要轮询。但是仍然有些古怪,因为检测任务通过奇怪的方式与反应线程通信。(译者注:下面的话挺绕的,可以参考原文)检测任务通过通知条件变量告诉反应线程等待的事件可能发生了,但是反应线程必须通过检查flag来确保事件发生了。检测线程置flag来告诉反应线程事件确实发生了但是检测线程仍然还要先需要通知条件变量,以唤醒反应线程来检查flag。这种方案是可以工作的但是不太优雅。
一个替代方案是让反应任务通过在检测任务设置的future上`wait`来避免使用条件变量互斥锁和flag。这可能听起来也是个古怪的方案。毕竟Item 38中说明了future代表了从被调用方通常是异步的调用方的通信的接收端,这里的检测任务和反应任务没有调用-被调用的关系。然而,Item 38中也说说明了通信新到发送端是`std::promise`,接收端是`future`不只能用在调用-被调用场景。这样的通信信道可以被在任何你需要从程序一个地方传递到另一个地方的场景。这里,我们用来在检测任务和反应任务之间传递信息,传递的信息就是感兴趣的事件是否已发生。 一个替代方案是让反应任务通过在检测任务设置的*future*上`wait`来避免使用条件变量互斥锁和flag。这可能听起来也是个古怪的方案。毕竟[Item38](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md)中说明了*future*代表了从被调用方(通常是异步的)调用方的通信信道的接收端,这里的检测任务和反应任务没有调用-被调用的关系。然而,[Item38](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md)中也说说明了发送端是个`std::promise`,接收端是个*future*的通信信道不是只能用在调用-被调用场景。这样的通信信道可以用在任何你需要从程序一个地方传递信息到另一个地方的场景。这里,我们用来在检测任务和反应任务之间传递信息,传递的信息就是感兴趣的事件已发生。
方案很简单。检测任务有一个`std::promise`对象(通信信道的写入),反应任务有对应的`std::future`(通信信道的读取)。当反应任务看到事件已经发生,设置`std::promise`对象(写入到通信信道)。同时,反应任务在`std::future`上等待。`wait`会锁住反应任务直到`std::promise`被设置。 方案很简单。检测任务有一个`std::promise`对象(即通信信道的写入端),反应任务有对应的*future*。当检测任务看到事件已经发生,设置`std::promise`对象(写入到通信信道)。同时,反应任务。`wait`会锁住反应任务直到`std::promise`被设置。
现在,`std::promise和futures(std::future and std::shared_future)`都是需要参数类型的模板。参数表明被传递的信息类型。在这里,没有数据被传递,只需要让反应任务知道`future`已经被设置了。我们需要的类型是表明在`std::promise`和`futures`之间没有数据被传递。所以选择`void`。检测任务使用`std::promise<void>`,反应任务使用`std::future<void> or std::shared_future<void>`。当感兴趣的事件发生时,检测任务设置`std::promise<void>`,反应任务在`futures`上等待。即使反应任务不接收任何数据,通信信道可以让反应任务知道检测任务是否设置了`void`数据(通过对`std::promise<void>`调用`set_value` 现在,`std::promise`和*futures*(即`std::future`和`std::shared_future`)都是需要类型形参的模板。形参表明通过通信信道被传递的信息的类型。在这里,没有数据被传递,只需要让反应任务知道它的*future*已经被设置了。我们在`std::promise`和*future*模板中需要的东西是表明通信信道中没有数据被传递的一个类型。这个类型就是`void`。检测任务使用`std::promise<void>`,反应任务使用`std::future<void>`或者`std::shared_future<void>`。当感兴趣的事件发生时,检测任务设置`std::promise<void>`,反应任务在*future*上`wait`。尽管反应任务不从检测任务那里接收任何数据,通信信道也可以让反应任务知道,检测任务什么时候已经通过对`std::promise<void>`调用`set_value`“写入”了`void`数据
所以,代码如下: 所以,
```cpp ```cpp
std::promise<void> p; // promise for communications channel std::promise<void> p; //通信信道的promise
``` ```
检测任务代码如下 检测任务代码很简洁
```cpp ```cpp
... // detect event … //检测某个事件
p.set_value(); // tell reacting task p.set_value(); //通知反应任务
``` ```
反应任务代码如下 反应任务代码也同样简单
```cpp ```cpp
... // prepare to react … //准备作出反应
p.get_future().wait(); // wait on future corresponding to p p.get_future().wait(); //等待对应于p的那个future
... //react to event … //对事件作出反应
``` ```
像使用flag的方法一样此设计不需要互斥锁无论检测任务是否在反应任务等待之前设置`std::promise`都可以工作,并且不受虚假唤醒的影响(只有条件变量才容易受到此影响)。与基于条件变量的方法一样,反应任务真是被blocked,不会一直占用系统资源。是不是很完美? 像使用flag的方法一样此设计不需要互斥锁无论在反应线程调用`wait`之前检测线程是否设置了`std::promise`都可以工作,并且不受虚假唤醒的影响(只有条件变量才容易受到此影响)。与基于条件变量的方法一样,反应任务在调用`wait`之后是真被阻塞住的,不会一直占用系统资源。是不是很完美?
当然不是,基于`future`的方法没有了上述问题但是有其他新的问题。比如Item 38中说明`std::promise 和 future`之间有共享状态,并且共享状态是动态分配的。因此你应该假定此设计会产生基于堆的分配和释放开销。 当然不是,基于*future*的方法没有了上述问题,但是有其他新的问题。比如,[Item38](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md)中说明,`std::promise`和*future*之间有个共享状态,并且共享状态是动态分配的。因此你应该假定此设计会产生基于堆的分配和释放开销。
也许更重要的是,`std::promise`只能设置一次。`std::promise 与 future`之间的通信是一次性不能重复使用。这是与基于条件变量或者flag的明显差异条件变量可以被重复通知flag也可以重复清除和设置。 也许更重要的是,`std::promise`只能设置一次。`std::promise`和*future*之间的通信是**一次性**的:不能重复使用。这是与基于条件变量或者基于flag的设计的明显差异,条件变量和flag都可以通信多次。条件变量可以被重复通知flag也可以重复清除和设置。
一次通信可能没有你想象中那么大的限制。假定你想创建一个挂起的线程以避免想要使用一个线程执行程序的时候的线程创建的开销。或者你想在线程运行前对其进行设置包括优先级和core affinity。C++并发API没有提供这种设置能力但是提供了`native_handle()`获取原始线程的接口通常获取的是POXIC或者Windows的线程这些低层次的API使你可以对线程设置优先级和 core affinity 一次通信可能没有你想象中那么大的限制。假定你想创建一个挂起的系统线程。就是,你想避免多次线程创建的那种经常开销,以便想要使用这个线程执行程序时,避免潜在的线程创建工作。或者你想创建一个挂起的线程,以便在线程运行前对其进行设置这样的设置包括优先级或者核心亲和性(*core affinity*。C++并发API没有提供这种设置能力但是`std::thread`提供了`native_handle`成员函数它的结果就是提供给你对平台原始线程API的访问通常是POSIX或者Windows的线程。这些低层次的API使你可以对线程设置优先级和亲和性
假设你仅仅想要挂起一次线程(在创建后,运行前),使用`void future`就是一个方案。代码如下 假设你仅仅想要对某线程挂起一次(在创建后,运行线程函数前),使用`void`的*future*就是一个可行方案。这是这个技术的关键点
```cpp ```cpp
std::promise<void> p; std::promise<void> p;
void react(); // func for reacting task void react(); //反应任务的函数
void detect() // func for detecting task void detect() //检测任务的函数
{ {
st::thread t([] // create thread std::thread t([] //创建线程
{ {
p.get_future().wait(); // suspend t until future is set p.get_future().wait(); //挂起t直到future被置位
react(); react();
}); });
... // here, t is suspended prior to call to react … //这里t在调用react前挂起
p.set_value(); // unsuspend t (and thus call react) p.set_value(); //解除挂起t因此调用react
... // do additional work … //做其他工作
t.join(); // make t unjoinable(see Item 37) t.join(); //使t不可结合见条款37
} }
``` ```
因为根据Item 37说明对于检测任务所有路径`thread`t都要是unjoinable的所以使用建议的`ThreadRAII`。代码如下: 因为所有离开`detect`的路径中`t`都要是不可结合的,所以使用类似于[Item37](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item37.md)中`ThreadRAII`的RAII类很明智。代码如下:
```cpp ```cpp
void detect() void detect()
{ {
ThreadRAII tr( ThreadRAII tr( //使用RAII对象
std::thread([] std::thread([]
{ {
p.get_future().wait(); p.get_future().wait();
react(); react();
}), }),
ThreadRAII::DtorAction::join // risky ! (see below) ThreadRAII::DtorAction::join //有危险!(见下)
); );
... // thread inside tr is suspended here … //tr中的线程在这里被挂起
p.set_value(); // unsuspend thread inside tr p.set_value(); //解除挂起tr中的线程
... // do additional work
} }
``` ```
这样看起来安全多了。问题在于第一个"..."区域注释了thread inside tr is suspended here如果异常发生`p.set_value()`永远不会调用,这意味着`lambda中的wait`永远不会返回即lambda不会结束问题就是因为RAII对象tr再析构函数中join。换句话说如果在第一个"..."中发生了异常函数挂起因为tr的析构不会被调用 这样看起来安全多了。问题在于第一个“…”区域中(注释了“`tr`中的线程在这里被挂起”的那句),如果异常发生,`p`上的`set_value`永远不会调用,这意味着*lambda*中的`wait`永远不会返回。那意味着在*lambda*中运行的线程不会结束这是个问题因为RAII对象`tr`在析构函数中被设置为在(`tr`中创建的)那个线程上实行`join`。换句话说,如果在第一个“…”区域中发生了异常,函数挂起,因为`tr`的析构函数永远无法完成
有很多方案解决这个问题,但是我把这个经验留给读者译者注http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html 中这个问题的讨论。这里我只想展示如何扩展原始代码不使用RAII类使其挂起然后取消挂起这不仅是个例是个通用场景。简单概括关键就是在反应任务的代码中使用`std::shared_future`代替`std::future`一旦你知道`std::future`的`share`成员函数将共享状态所有权转移到`std::shared_future`中,代码自然就写出来了。唯一需要注意的是,每个反应线程需要处理自己的`std::shared_future`副本,该副本引用共享状态,因此通过`share`获得的`shared_future`要被lambda按值捕获 有很多方案解决这个问题,但是我把这个经验留给读者。(一个开始研究这个问题的好地方是我的博客*[The View From Aristeia](http://scottmeyers.blogspot.com/)*中2013年12月24日的文章“[ThreadRAII + Thread Suspension = Trouble?](http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html)”这里我只想展示如何扩展原始代码即不使用RAII类使其挂起然后取消挂起不仅一个反应任务而是多个任务。简单概括关键就是在`react`的代码中使用`std::shared_future`代替`std::future`一旦你知道`std::future`的`share`成员函数将共享状态所有权转移到`share`产生的`std::shared_future`中,代码自然就写出来了。唯一需要注意的是,每个反应线程需要自己的`std::shared_future`副本,该副本引用共享状态,因此通过`share`获得的`shared_future`要被在反应线程中运行的*lambda*按值捕获:
```cpp ```cpp
std::promise<void> p; // as before std::promise<void> p; //跟之前一样
void detect() // now for multiple reacting tasks void detect() //现在针对多个反映线程
{ {
auto sf = g.get_future().share(); // sf's type is std::shared_future<void> auto sf = p.get_future().share(); //sf的类型是std::shared_future<void>
std::vector<std::thread> vt; // container for reacting threads std::vector<std::thread> vt; //反应线程容器
for (int i = 0; i < threadsToRun; ++i) for (int i = 0; i < threadsToRun; ++i) {
{ vt.emplace_back([sf]{ sf.wait(); //在sf的局部副本上wait
vt.emplace_back([sf]{ react(); }); //emplace_back见条款42
sf.wait(); }
react(); … //如果这个“…”抛出异常detect挂起
}); // wait on local copy of sf; see Item 43 for info on emplace_back p.set_value(); //所有线程解除挂起
}
... // detect hangs if this "..." code throws ! for (auto& t : vt) { //使所有线程不可结合;
p.set_value(); // unsuspend all threads t.join(); //“auto&”见条款2
... }
for (auto& t : vt) {
t.join(); // make all threads unjoinable: see Item2 for info on "auto&"
}
} }
``` ```
这样`future`就可以达到预期效果了,这就是你应该将其应用于一次通信的原因。 使用*future*的设计可以实现这个功能值得注意,这也是你应该考虑将其应用于一次通信的原因。
**请记住:**
- 对于简单的事件通信,基于条件变量的设计需要一个多余的互斥锁,对检测和反应任务的相对进度有约束,并且需要反应任务来验证事件是否已发生。
### 需要记住的事 - 基于flag的设计避免的上一条的问题但是是基于轮询而不是阻塞。
- 条件变量和flag可以组合使用但是产生的通信机制很不自然。
- 对于简单的事件通信,条件变量需要一个多余的互斥锁,对检测和反应任务的相对进度有约束,并且需要反应任务来验证事件是否已发生 - 使用`std::promise`和*future*的方案避开了这些问题,但是这个方法使用了堆内存存储共享状态,同时有只能使用一次通信的限制。
- 基于flag的设计避免的上一条的问题但是不是真正的挂起反应任务
- 组合条件变量和flag使用上面的问题都解决了但是逻辑不让人愉快
- 使用`std::promise和future`的方案,要考虑堆内存的分配和销毁开销,同时有只能使用一次通信的限制