mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2025-02-05 00:30:31 +08:00
Update item36.md
This commit is contained in:
parent
dc47a11efd
commit
badd662b92
@ -1,120 +1,135 @@
|
||||
## Item 36:Specify std::launch::async if asynchronicity is essential
|
||||
## 条款三十六:如果有异步的必要请指定`std::launch::async`
|
||||
|
||||
## Item36: 确保在异步为必须时,才指定`std::launch::async`
|
||||
**Item 36: Specify `std::launch::async` if asynchronicity is essential.**
|
||||
|
||||
当你调用`std::async`执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你想要`std::async`执行的操作。你确实通过`std::async`launch policy(译者注:这里没有翻译)要求执行函数,有两种标准policy,都通过`std::launch`域的枚举类型表示(参见Item10关于枚举的更多细节)。假定一个函数**f**传给`std::async`来执行:
|
||||
当你调用`std::async`执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你要求`std::async`执行的操作。你事实上要求这个函数按照`std::async`启动策略来执行。有两种标准策略,每种都通过`std::launch`这个限域`enum`的一个枚举名表示(关于枚举的更多细节参见[Item10](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item10.md))。假定一个函数`f`传给`std::async`来执行:
|
||||
|
||||
- **`std::launch::async`的launch policy**意味着f必须异步执行,即在不同的线程
|
||||
- **`std::launch::deferred`的launch policy**意味着f仅仅在当调用`get或者wait`要求`std::async`的返回值时才执行。这表示f推迟到被求值才延迟执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当`get或wait`被调用,f会同步执行,即调用方停止直到f运行结束。如果`get和wait`都没有被调用,f将不会被执行
|
||||
- **`std::launch::async`启动策略**意味着`f`必须异步执行,即在不同的线程。
|
||||
- **`std::launch::deferred`启动策略**意味着`f`仅当在`std::async`返回的*future*上调用`get`或者`wait`时才执行。这表示`f`**推迟**到存在这样的调用时才执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当`get`或`wait`被调用,`f`会同步执行,即调用方被阻塞,直到`f`运行结束。如果`get`和`wait`都没有被调用,`f`将不会被执行。(这是个简化说法。关键点不是要在其上调用`get`或`wait`的那个*future*,而是*future*引用的那个共享状态。([Item38](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item38.md)讨论了*future*与共享状态的关系。)因为`std::future`支持移动,也可以用来构造`std::shared_future`,并且因为`std::shared_future`可以被拷贝,对共享状态——对`f`传到的那个`std::async`进行调用产生的——进行引用的*future*对象,有可能与`std::async`返回的那个*future*对象不同。这非常绕口,所以经常回避这个事实,简称为在`std::async`返回的*future*上调用`get`或`wait`。)
|
||||
|
||||
有趣的是,`std::async`的默认launch policy是以上两种都不是。相反,是求或在一起的。下面的两种调用含义相同
|
||||
可能让人惊奇的是,`std::async`的默认启动策略——你不显式指定一个策略时它使用的那个——不是上面中任意一个。相反,是求或在一起的。下面的两种调用含义相同:
|
||||
|
||||
```cpp
|
||||
auto fut1 = std::async(f); // run f using default launch policy
|
||||
auto fut2 = std::async(std::launch::async | std::launch::deferred, f); // run f either async or defered
|
||||
auto fut1 = std::async(f); //使用默认启动策略运行f
|
||||
auto fut2 = std::async(std::launch::async | //使用async或者deferred运行f
|
||||
std::launch::deferred,
|
||||
f);
|
||||
```
|
||||
|
||||
因此默认策略允许f异步或者同步执行。如同Item 35中指出,这种灵活性允许`std::async`和标准库的线程管理组件(负责线程的创建或销毁)避免超载。这就是使用`std::async`并发编程如此方便的原因。
|
||||
因此默认策略允许`f`异步或者同步执行。如同[Item35](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md)中指出,这种灵活性允许`std::async`和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。这就是使用`std::async`并发编程如此方便的原因。
|
||||
|
||||
但是,使用默认启动策略的`std::async`也有一些有趣的影响。给定一个线程t执行此语句:
|
||||
但是,使用默认启动策略的`std::async`也有一些有趣的影响。给定一个线程`t`执行此语句:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(f); // run f using default launch policy
|
||||
auto fut = std::async(f); //使用默认启动策略运行f
|
||||
```
|
||||
|
||||
- 无法预测f是否会与t同时运行,因为f可能被安排延迟运行
|
||||
- 无法预测f是否会在调用`get或wait`的线程上执行。如果那个线程是t,含义就是无法预测f是否也在线程t上执行
|
||||
- 无法预测f是否执行,因为不能确保`get或者wait`会被调用
|
||||
- **无法预测`f`是否会与`t`并发运行**,因为`f`可能被安排延迟运行。
|
||||
- **无法预测`f`是否会在与某线程相异的另一线程上执行,这个某线程在`fut`上调用`get`或`wait`**。如果对`fut`调用函数的线程是`t`,含义就是无法预测`f`是否在异于`t`的另一线程上执行。
|
||||
- **无法预测`f`是否执行**,因为不能确保在程序每条路径上,都会不会在`fut`上调用`get`或者`wait`。
|
||||
|
||||
默认启动策略的调度灵活性导致使用线程本地变量比较麻烦,因为这意味着如果f读写了线程本地存储(thread-local storage, TLS),不可能预测到哪个线程的本地变量被访问:
|
||||
默认启动策略的调度灵活性导致使用`thread_local`变量比较麻烦,因为这意味着如果`f`读写了**线程本地存储**(*thread-local storage*,TLS),不可能预测到哪个线程的变量被访问:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(f); // TLS for f possibly for independent thread, but possibly for thread invoking get or wait on fut
|
||||
auto fut = std::async(f); //f的TLS可能是为单独的线程建的,
|
||||
//也可能是为在fut上调用get或者wait的线程建的
|
||||
```
|
||||
|
||||
还会影响到基于超时机制的wait循环,因为在task的`wait_for`或者`wait_until`调用中(参见Item 35)会产生延迟求值(`std::launch::deferred`)。意味着,以下循环看似应该终止,但是实际上永远运行:
|
||||
这还会影响到基于`wait`的循环使用超时机制,因为在一个延时的*task*(参见[Item35](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/Item35.md))上调用`wait_for`或者`wait_until`会产生`std::launch::deferred`值。意味着,以下循环看似应该最终会终止,但可能实际上永远运行:
|
||||
|
||||
```cpp
|
||||
using namespace std::literals; // for C++14 duration suffixes; see Item 34
|
||||
void f()
|
||||
using namespace std::literals; //为了使用C++14中的时间段后缀;参见条款34
|
||||
|
||||
void f() //f休眠1秒,然后返回
|
||||
{
|
||||
std::this_thread::sleep_for(1s);
|
||||
}
|
||||
|
||||
auto fut = std::async(f);
|
||||
while (fut.wait_for(100ms) != std::future_status::ready)
|
||||
{ // loop until f has finished running... which may never happen!
|
||||
...
|
||||
auto fut = std::async(f); //异步运行f(理论上)
|
||||
|
||||
while (fut.wait_for(100ms) != //循环,直到f完成运行时停止...
|
||||
std::future_status::ready) //但是有可能永远不会发生!
|
||||
{
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
如果f与调用`std::async`的线程同时运行(即,如果为f选择的启动策略是`std::launch::async`),这里没有问题(假定f最终执行完毕),但是如果f是延迟执行,`fut.wait_for`将总是返回`std::future_status::deferred`。这表示循环会永远执行下去。
|
||||
如果`f`与调用`std::async`的线程并发运行(即,如果为`f`选择的启动策略是`std::launch::async`),这里没有问题(假定`f`最终会执行完毕),但是如果`f`是延迟执行,`fut.wait_for`将总是返回`std::future_status::deferred`。这永远不等于`std::future_status::ready`,循环会永远执行下去。
|
||||
|
||||
这种错误很容易在开发和单元测试中忽略,因为它可能在负载过高时才能显现出来。当机器负载过重时,任务推迟执行才最有可能发生。毕竟,如果硬件没有超载,没有理由不安排任务并发执行。
|
||||
这种错误很容易在开发和单元测试中忽略,因为它可能在负载过高时才能显现出来。那些是使机器资源超额或者线程耗尽的条件,此时任务推迟执行才最有可能发生。毕竟,如果硬件没有资源耗尽,没有理由不安排任务并发执行。
|
||||
|
||||
修复也是很简单的:只需要检查与`std::async`的future是否被延迟执行即可,那样就会避免进入无限循环。不幸的是,没有直接的方法来查看future是否被延迟执行。相反,你必须调用一个超时函数----比如`wait_for`这种函数。在这个逻辑中,你不想等待任何事,只想查看返回值是否`std::future_status::deferred`,如果是就使用0调用`wait_for`来终止循环。
|
||||
修复也是很简单的:只需要检查与`std::async`对应的`future`是否被延迟执行即可,那样就会避免进入无限循环。不幸的是,没有直接的方法来查看`future`是否被延迟执行。相反,你必须调用一个超时函数——比如`wait_for`这种函数。在这个情况中,你不想等待任何事,只想查看返回值是否是`std::future_status::deferred`,所以无须怀疑,使用0调用`wait_for`:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(f);
|
||||
if (fut.wait_for(0s) == std::future_status::deferred) { // if task is deferred
|
||||
... // use wait or get on fut to call f synchronously
|
||||
}
|
||||
else { // task isn't deferred
|
||||
while(fut.wait_for(100ms) != std::future_status::ready) { // infinite loop not possible(assuming f finished)
|
||||
... // task is neither deferred nor ready, so do concurrent word until it's ready
|
||||
auto fut = std::async(f); //同上
|
||||
|
||||
if (fut.wait_for(0s) == //如果task是deferred(被延迟)状态
|
||||
std::future_status::deferred)
|
||||
{
|
||||
… //在fut上调用wait或get来异步调用f
|
||||
} else { //task没有deferred(被延迟)
|
||||
while (fut.wait_for(100ms) != //不可能无限循环(假设f完成)
|
||||
std::future_status::ready) {
|
||||
… //task没deferred(被延迟),也没ready(已准备)
|
||||
//做并行工作直到已准备
|
||||
}
|
||||
… //fut是ready(已准备)状态
|
||||
}
|
||||
```
|
||||
|
||||
这些各种考虑的结果就是,只要满足以下条件,`std::async`的默认启动策略就可以使用:
|
||||
|
||||
- task不需要和执行`get or wait`的线程并行执行
|
||||
- 不会读写线程的线程本地变量
|
||||
- 可以保证在`std::async`返回的将来会调用`get or wait`,或者该任务可能永远不会执行是可以接受的
|
||||
- 使用`wait_for or wait_until`编码时考虑deferred状态
|
||||
- *task*不需要和执行`get`或`wait`的线程并行执行。
|
||||
- 读写哪个线程的`thread_local`变量没什么问题。
|
||||
- 可以保证会在`std::async`返回的*future*上调用`get`或`wait`,或者该任务可能永远不会执行也可以接受。
|
||||
- 使用`wait_for`或`wait_until`编码时考虑到了延迟状态。
|
||||
|
||||
如果上述条件任何一个都满足不了,你可能想要保证`std::async`的任务真正的异步执行。进行此操作的方法是调用时,将`std::launch::async`作为第一个参数传递:
|
||||
如果上述条件任何一个都满足不了,你可能想要保证`std::async`会安排*task*进行真正的异步执行。进行此操作的方法是调用时,将`std::launch::async`作为第一个参数传递:
|
||||
|
||||
```cpp
|
||||
auto fut = std::async(std::launch::async, f); // launch f asynchronously
|
||||
auto fut = std::async(std::launch::async, f); //异步启动f的执行
|
||||
```
|
||||
|
||||
事实上,具有类似`std::async`行为的函数,但是会自动使用`std::launch::async`作为启动策略的工具也是很容易编写的,C++11版本如下:
|
||||
事实上,对于一个类似`std::async`行为的函数,但是会自动使用`std::launch::async`作为启动策略的工具,拥有它会非常方便,而且编写起来很容易也使它看起来很棒。C++11版本如下:
|
||||
|
||||
```cpp
|
||||
template<typename F, typename... Ts>
|
||||
inline
|
||||
std::future<typename std::result_of<F(Ts...)>::type>
|
||||
reallyAsync(F&& f, Ts&&... params)
|
||||
reallyAsync(F&& f, Ts&&... params) //返回异步调用f(params...)得来的future
|
||||
{
|
||||
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
|
||||
return std::async(std::launch::async,
|
||||
std::forward<F>(f),
|
||||
std::forward<Ts>(params)...);
|
||||
}
|
||||
```
|
||||
|
||||
这个函数接受一个可调用对象和0或多个参数params然后完美转发(参见Item25)给`std::async`,使用`std::launch::async`作为启动参数。就像`std::async`一样,返回`std::future`类型。确定结果的类型很容易,因为类型特征`std::result_of`可以提供(参见Item 9 关于类型特征的详细表述)。
|
||||
这个函数接受一个可调用对象`f`和0或多个参数`params`,然后完美转发(参见[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md))给`std::async`,使用`std::launch::async`作为启动策略。就像`std::async`一样,返回`std::future`作为用`params`调用`f`得到的结果。确定结果的类型很容易,因为*type trait* `std::result_of`可以提供给你。(参见[Item9](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item9.md)关于*type trait*的详细表述。)
|
||||
|
||||
`reallyAsync`就像`std::async`一样使用:
|
||||
|
||||
```cpp
|
||||
auto fut = reallyAsync(f);
|
||||
auto fut = reallyAsync(f); //异步运行f,如果std::async抛出异常它也会抛出
|
||||
```
|
||||
|
||||
在C++14中,返回类型的推导能力可以简化函数的定义:
|
||||
在C++14中,`reallyAsync`返回类型的推导能力可以简化函数的声明:
|
||||
|
||||
```cpp
|
||||
template<typename f, typename... Ts>
|
||||
template<typename F, typename... Ts>
|
||||
inline
|
||||
auto
|
||||
auto // C++14
|
||||
reallyAsync(F&& f, Ts&&... params)
|
||||
{
|
||||
return std::async(std::launch::async, std::forward<T>(f), std::forward<Ts>(params)...);
|
||||
return std::async(std::launch::async,
|
||||
std::forward<F>(f),
|
||||
std::forward<Ts>(params)...);
|
||||
}
|
||||
```
|
||||
|
||||
这个版本清楚表明,`reallyAsync`除了使用`std::launch::async`启动策略之外什么也没有做。
|
||||
|
||||
### 需要记住的事
|
||||
**请记住:**
|
||||
|
||||
- `std::async`的默认启动策略是异步或者同步的
|
||||
- 灵活性导致访问**thread_locals**的不确定性,隐含了task可能不会被执行的意思,会影响程序基于`wait`的超时逻辑
|
||||
- 只有确实异步时才指定`std::launch::async`
|
||||
- `std::async`的默认启动策略是异步和同步执行兼有的。
|
||||
- 这个灵活性导致访问`thread_local`s的不确定性,隐含了*task*可能不会被执行的意思,会影响调用基于超时的`wait`的程序逻辑。
|
||||
- 如果异步执行*task*非常关键,则指定`std::launch::async`。
|
||||
|
Loading…
Reference in New Issue
Block a user