条款三十六:如果有异步的必要请指定std::launch::async
Item 36: Specify std::launch::async
if asynchronicity is essential.
当你调用std::async
执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你要求std::async
执行的操作。你事实上要求这个函数按照std::async
启动策略来执行。有两种标准策略,每种都通过std::launch
这个限域enum
的一个枚举名表示(关于枚举的更多细节参见Item10)。假定一个函数f
传给std::async
来执行:
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讨论了future与共享状态的关系。)因为std::future
支持移动,也可以用来构造std::shared_future
,并且因为std::shared_future
可以被拷贝,对共享状态——对f
传到的那个std::async
进行调用产生的——进行引用的future对象,有可能与std::async
返回的那个future对象不同。这非常绕口,所以经常回避这个事实,简称为在std::async
返回的future上调用get
或wait
。)
可能让人惊奇的是,std::async
的默认启动策略——你不显式指定一个策略时它使用的那个——不是上面中任意一个。相反,是求或在一起的。下面的两种调用含义相同:
auto fut1 = std::async(f); //使用默认启动策略运行f
auto fut2 = std::async(std::launch::async | //使用async或者deferred运行f
std::launch::deferred,
f);
因此默认策略允许f
异步或者同步执行。如同Item35中指出,这种灵活性允许std::async
和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。这就是使用std::async
并发编程如此方便的原因。
但是,使用默认启动策略的std::async
也有一些有趣的影响。给定一个线程t
执行此语句:
auto fut = std::async(f); //使用默认启动策略运行f
- 无法预测
f
是否会与t
并发运行,因为f
可能被安排延迟运行。 - 无法预测
f
是否会在与某线程相异的另一线程上执行,这个某线程在fut
上调用get
或wait
。如果对fut
调用函数的线程是t
,含义就是无法预测f
是否在异于t
的另一线程上执行。 - 无法预测
f
是否执行,因为不能确保在程序每条路径上,都会不会在fut
上调用get
或者wait
。
默认启动策略的调度灵活性导致使用thread_local
变量比较麻烦,因为这意味着如果f
读写了线程本地存储(thread-local storage,TLS),不可能预测到哪个线程的变量被访问:
auto fut = std::async(f); //f的TLS可能是为单独的线程建的,
//也可能是为在fut上调用get或者wait的线程建的
这还会影响到基于wait
的循环使用超时机制,因为在一个延时的任务(参见Item35)上调用wait_for
或者wait_until
会产生std::launch::deferred
值。意味着,以下循环看似应该最终会终止,但可能实际上永远运行:
using namespace std::literals; //为了使用C++14中的时间段后缀;参见条款34
void f() //f休眠1秒,然后返回
{
std::this_thread::sleep_for(1s);
}
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
。这永远不等于std::future_status::ready
,循环会永远执行下去。
这种错误很容易在开发和单元测试中忽略,因为它可能在负载过高时才能显现出来。那些是使机器资源超额或者线程耗尽的条件,此时任务推迟执行才最有可能发生。毕竟,如果硬件没有资源耗尽,没有理由不安排任务并发执行。
修复也是很简单的:只需要检查与std::async
对应的future
是否被延迟执行即可,那样就会避免进入无限循环。不幸的是,没有直接的方法来查看future
是否被延迟执行。相反,你必须调用一个超时函数——比如wait_for
这种函数。在这个情况中,你不想等待任何事,只想查看返回值是否是std::future_status::deferred
,所以无须怀疑,使用0调用wait_for
:
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
的默认启动策略就可以使用:
- 任务不需要和执行
get
或wait
的线程并行执行。 - 读写哪个线程的
thread_local
变量没什么问题。 - 可以保证会在
std::async
返回的future上调用get
或wait
,或者该任务可能永远不会执行也可以接受。 - 使用
wait_for
或wait_until
编码时考虑到了延迟状态。
如果上述条件任何一个都满足不了,你可能想要保证std::async
会安排任务进行真正的异步执行。进行此操作的方法是调用时,将std::launch::async
作为第一个实参传递:
auto fut = std::async(std::launch::async, f); //异步启动f的执行
事实上,对于一个类似std::async
行为的函数,但是会自动使用std::launch::async
作为启动策略的工具,拥有它会非常方便,而且编写起来很容易也使它看起来很棒。C++11版本如下:
template<typename F, typename... Ts>
inline
std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&&... params) //返回异步调用f(params...)得来的future
{
return std::async(std::launch::async,
std::forward<F>(f),
std::forward<Ts>(params)...);
}
这个函数接受一个可调用对象f
和0或多个形参params
,然后完美转发(参见Item25)给std::async
,使用std::launch::async
作为启动策略。就像std::async
一样,返回std::future
作为用params
调用f
得到的结果。确定结果的类型很容易,因为type trait std::result_of
可以提供给你。(参见Item9关于type trait的详细表述。)
reallyAsync
就像std::async
一样使用:
auto fut = reallyAsync(f); //异步运行f,如果std::async抛出异常它也会抛出
在C++14中,reallyAsync
返回类型的推导能力可以简化函数的声明:
template<typename F, typename... Ts>
inline
auto // C++14
reallyAsync(F&& f, Ts&&... params)
{
return std::async(std::launch::async,
std::forward<F>(f),
std::forward<Ts>(params)...);
}
这个版本清楚表明,reallyAsync
除了使用std::launch::async
启动策略之外什么也没有做。
请记住:
std::async
的默认启动策略是异步和同步执行兼有的。- 这个灵活性导致访问
thread_local
s的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的wait
的程序逻辑。 - 如果异步执行任务非常关键,则指定
std::launch::async
。