建议先阅读 C++ 之父的 A tour of C++ 中相关章节
35 优先选用基于任务而非基于线程的程序设计
以异步方式运行函数 int doWork()
,有两种选择:
- 基于线程:
thread t(doWork);
- 基于任务:
auto fut = async(doWork);
理由
- 更容易取得返回值,async 返回 future,可以通过 get() 结果;thread 没有办法直接取得返回值
- 异常安全:如果 doWork() 抛异常,基于线程的程序会直接挂掉(std::terminate);基于任务返回的 future 则可存储、并有机会处理异常
- 任务是更高阶的抽象,把程序员从线程耗尽、超订、负载均衡等线程管理的细节中解放出来
- 系统线程数是有限的资源,可能会线程耗尽,抛出 std::system_error 异常;async 不保证创建新的线程,在无线程可用或超订情况下,可能在请求结果(fut.get() 或 wait())的线程中执行 doWork()
- 如果一定需要在另外的线程中执行,如不想阻塞 GUI 线程,可以在调用 async 时传入 launch::async 启动策略参数
- async 能够和运行时调度器配合,根据线程上下文切换成本、CPU缓存命中率、系统中所有进程的线程实时负载情况等,自动选择最佳的线程策略,保证硬件满载工作的同时,避免超订(over subscription)
- 超订:非阻塞的线程数超过硬件线程数,系统需要给线程分配时间片,进行上下文切换,增加总体线程管理开销。尤其是当线程被调度器切换到另一个 CPU 核上时,大概率缓存失效
- 负载均衡
- 最高水平的线程调度器会使用全系统范围的线程池和工作窃取算法来提高硬件内核间的负载均衡
- 系统线程数是有限的资源,可能会线程耗尽,抛出 std::system_error 异常;async 不保证创建新的线程,在无线程可用或超订情况下,可能在请求结果(fut.get() 或 wait())的线程中执行 doWork()
适合用线程而非任务的场景(不常见,慎用)
- 需要访问底层平台 API:底层 API 更丰富(如C++没有线程优先级和亲和性概念)。
std::thread::native_handle
可以返回底层线程API,async/future 不可以 - 需要且有能力为应用优化线程:如开发的某个服务器软件,作为唯一的主要进程部署在特定机器上
总结
std::thread
没有直接获取异步函数返回值的途径,如果异步函数抛异常,则程序终止- 基于线程的程序设计需要考虑线程耗尽、超订、负载均衡以及新平台适配
- 基于任务(应用 async 默认启动策略)的程序设计可以解决上述问题