条款36 :如果异步是必要的,则指定 std::launch::async
该条款的核心论点是,std::async 的默认启动策略虽然为标准库调度器提供了有用的灵活性,但也给程序员带来了不确定性,可能导致难以发现的bug。因此,当你确实需要函数必须异步执行时,就应该明确指定这一点。
std::async 的启动策略
std::async 可以接受一个启动策略参数,来控制任务的执行方式: - std::launch::async:此策略保证任务会在一个不同的线程上异步执行 。 - std::launch::deferred:此策略意味着任务会被延迟执行。它只会在返回的 std::future 对象上调用 get() 或 wait() 时,才同步地(即阻塞地)在调用 get() 或 wait() 的那个线程上执行。如果 get() 或 wait() 一直未被调用,任务将永远不会执行 。 - 默认启动策略:如果你不指定任何策略,std::async 会使用 std::launch::async | std::launch::deferred 的组合策略 。
默认的组合策略赋予了标准库调度器极大的灵活性,让它可以根据系统当前的线程负载情况来决定任务的执行方式,这有助于避免线程耗尽和超订问题(如条款35所述)。然而,这种灵活性也给程序员带来了几个严重的不确定性:
- 无法预知任务是否并发运行:你无法保证任务 f 会与调用 std::async 的线程 t 并发执行 。
- 无法预知任务运行在哪一个线程上:你无法确定任务 f 是运行在一个新线程上,还是运行在对 future 调用 get() 或 wait() 的那个线程上 。这对 thread_local 变量的使用会产生影响 。
- 无法保证任务一定会被执行:如果 std::async 返回的 future 在程序的任何路径上都未被调用 get() 或 wait(),那么被延迟执行的任务可能永远不会启动。
核心陷阱:基于超时的 wait 与无限循环
默认策略最危险的陷阱在于它与基于超时的等待函数(如 wait_for)的交互
1 | using namespace std::literals; |
如果调度器因为系统负载过高等原因,为任务 f 选择了 std::launch::deferred 策略,那么 fut.wait_for(…) 的返回值将永远是 std::future_status::deferred。这也就意味着, fut.wait_for(…) 的结果永远不等于 std::future_status::ready,导致 while 循环成为一个无限循环。这个bug非常隐蔽,因为它可能只在高负载的生产环境中才会出现。
解决方案:显式指定启动策略
该条款给出的结论非常明确:如果你需要确保任务必须异步执行,从而避免上述所有不确定性和陷阱,你就必须在调用 std::async 时显式指定 std::launch::async 启动策略。
1 | auto fut = std::async(std::launch::async, f); // 保证 f 会异步执行 |