如果你想以异步方式运行函数doAsyncWork()
,有两种方法:
- 将doAsyncWork()传递给std::thread对象,这种方式属于基于线程的程序设计
int doAsyncWork(); std::thread t(doAsyncWork);
- 也可以将doAsyncWork()传递给std::async对象,这种策略叫做基于任务
auto fut = std::async(doAsyncWork); //fut是期望的缩写
就像标题所说的那样,在方式选择时,优先选用基于任务而非基于线程的程序设计,原因如下:
1、 更易于获得返回值
我们这里假定,我们对doAsyncWork函数的返回值感兴趣,在基于线程的调用中,没有什么方法可以直接获取该函数的返回值。
而在基于任务的调用中,却很容易,因为std::async的返回值std::future提供了get()函数,可以轻松获取函数执行结果。
而且如果doAsyncWork函数在调用期间,发射了一个异常,std::async的返回值std::future的get函数也能轻松访问到该异常;但如果采用基于线程的途径,在doAsyncWork抛出异常时,程序将直接崩溃。
2、具有更高层的抽象
基于线程和基于任务的程序设计之间更基本的区别在于,基于任务的程序设计表现着更高阶的抽象。它把你从线程管理的细节里解放出来。
3、超订问题
软件线程是一种有限的资源,如果你试图创建的线程数量多于系统能够提供的数量,就会抛出std::system_error异常。这一点无论如何都会成立,即使待运行函数已经使用noexcept声明不能抛出异常:
int doAsyncWork() noexcept;
但实际上,以下这条语句还是可能会抛出异常:
std::thread t(doAsyncWork); //若已无可用线程,则抛出异常
而且即使没有用尽线程,还是有可能会发生超订(oversubscription)问题,即在就绪状态(即非阻塞)的软件线程超过了硬件线程数量时,线程调度器会为软件按线程在硬件线程之上分配CPU时间片。当一个线程时间片用完,另一个线程启动时,就会执行语境切换。这种语境切换会增加系统的总体线程管理开销,尤其在一个软件线程的这一次和下一次被调度器切换到不同的CPU内核上的硬件线程时会发生高昂的计算成本(1) 软件线程通常不会命中CPU缓存 (2)CPU内核运行的新线程还会污染CPU上存储的旧线程的缓存。
避免超订是困难的,但是如果把这些问题抛给别人去做,你的生活就可以轻松起来。而std::async正是做到了这一点:
auto fut = std::async(doAsyncWork); //由标准库的实现者负责线程管理
因为在std::async()使用默认启动策略运行任务时,其不保证会创建一个新的软件线程,也就是说它允许线程调度器把doAsyncWork函数运行在请求doAsyncWork函数结果的线程上(调用了get或wait的线程),如果此时系统发生了超订或线程耗尽,则线程调度器就可以利用其这个特性,完成任务。
最高水平的线程调度器会使用全系统范围的线程池或工作窃取算法来避免超定问题。
比起基于线程编程,基于任务的设计能够分担你手工管理线程的艰辛,而且它提供了一种很自然的方式,让你检查异步执行函数的结果(即,返回值或者异常)。但是仍有几种情况下,直接受用线程会更合适,他们包括:
- 你需要访问底层线程实现的API: C++并发API通常会采用特定平台的低级API来实现,经常使用的有pthread或者Windows线程库。它们提供的API比C++提供的更丰富(例如,C++没有线程优先级和亲和性的概念)。为了访问底层线程实现的API,std::thread通常会提供native_handle成员函数,而std::future则没有该功能的对应物。
- 你需要且有能力为你的应用优化线程用法:这也是有可能的,例如,你开发的是个服务器软件,执行时的性能剖析情况已知,并且作为唯一的主要进程部署在一种硬件特性固定的机器平台上。
- 你需要实现超越C++并发API的线程技术: 例如,在C++实现中并未提供的线程池的平台上实现线程池。
无论如何,这些都是不常见的情况。大多数时候,你应该选择基于任务的设计,而非直接进行线程相关的程序设计。