C++11新特性之十四:std::async

如果你想异步地运行函数doAsyncWork,你有两个基本的选择。你可以创建一个std::thread,用它来运行doAsyncWork,这是基于线程(thread-based)的方法:

int doAsyncWork();
std::thread t(doAsyncWork);

或者你把doAsynWork传递给std::async,一个叫做基于任务(task-based)的策略:

auto fut = std::async(doAsyncWork);    // "fut"的意思是"future"

在这种调用中,传递给std::async的函数对象被认为是一个任务(task)。

基于任务的方法通常优于基于线程的对应方法,我们看到基于任务的方法代码量更少,这已经是展示了一些原因了。在这里,doAsyncWork会返回一个值,我们有理由假定调用doAsyncWork的代码对这个返回值有兴趣。在基于线程的调用中,没有直接的办法获取到它;而在基于任务的调用中,这很容易,因为std::asyn返回的future提供了一个函数get来获取返回值。如果doAsyncWork函数发出了一个异常,get函数是很重要的,它能取到这个异常。在基于线程的方法中,如果doAsyncWork抛出了异常,程序就死亡了(借助std::terminate)。

基于线程编程和基于任务编程的一个更根本的区别是,基于任务编程表现出更高级别的抽象。它让你摆脱线程管理的细节。

软件线程是一种受限的资源,如果你想创建的线程数量多于系统提供的数量,会抛出std::system_error异常。就算你规定函数不能抛出异常,这个异常也会抛出。例如,就算你把doAsyncWork声明为noexcept。

int doAsyncWork noexcept;   // 关于noexcept

这语句还是可能会抛出异常:

std::thread t(doAsyncWork);  // 如果没有可获取的系统,就抛出异常

写得好的软件必须想个办法解决这个可能性,但如何解决呢?一个办法是在当前线程运行doAsyncWork,但这会导致负载不均衡的问题,而且,如果当前线程是个GUI线程,会导致响应时间变长。另一个方法是等待某些已存在的软件线程完成工作,然后再尝试创建一个新的std::thread对象,但是有可能发生这种事情:已存在的线程在等待doAsyncWork的处理(例如,doAsyncWorkd的返回值,或者通知条件变量)。

如果你把这些问题扔给某个人去做,你的生活就很惬意啦,然后使用std::async就能显式地做这件事:

auto fut = std::async(doAsyncWork);  // 线程管理的责任交给标准库的实现者

这个调用把线程管理的责任转交给C++标准库的实现者。例如,得到线程数超标异常的可能性显著减少,因为这个调用可能从不产生这个异常。“它是怎样做到的呢?”你可能好奇,“如果我申请多于系统提供的线程数,使用std::thread和使用std::async有区别吗?”答案是有区别,因为当用默认启动策略(default launch policy)调用std::async时,不能保证它会创建一个新的软件线程。而且,它允许调度器把指定函数(本例中的doAsyncWork)运行在——请求doAsyncWork结果的线程中(例如,那个线程调用了get或者对fut使用wait ),如果系统oversubsrcibed(过载)或线程数耗尽时,合理的调度器可以利用这个优势。

当你调用std::async来执行一个函数(或一个可执行对象)时,你通常希望函数是异步执行的。但你没有要求std::async必须这样做,函数是根据std::async的发射策略(launch policy)来执行的。有两个标准策略,每个都是通过std::launch局部枚举(scoped enum, 看条款10)来表示。假设一个函数f要传递给std::launch执行,
std::launch::async发射策略意味着函数f必须异步执行,即在另一线程执行。
std::launch::deferred发射策略意味着函数f可能只会在——std::async返回的future对象调用get或wait时——执行。那就是,执行会推迟到其中一个调用发生。当调用get或wait时,f会同步执行,即,调用者会阻塞直到f运行结束。如果get或wait没有被调用,f就绝对不会执行。
可能很奇怪,std::async的默认发射策略——它的默认策略是你不能显式指定的——不是两者其中的一种,相反,是两者进行或运算。下面两个函数完全是相同的意思:

auto fut1 = std::async(f);       // 使用默认发射策略执行f

auto fut2 = std::async(std::launch::async |     // 使用async或deferred执行f
                       std::launch::deferred
                       f);

默认的发射策略允许异步或同步执行函数f。也就是说,没有办法预知函数f是否运行在与调用get或wait函数所在的线程不同的线程。例如:

using namespace std::literals;        

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的线程并发执行(即,使用std::launch::async发射策略),这里就没有问题(假设f能结束执行,不会一直死循环)。但如果f被推迟(deferred),fut.wait_for将总是返回std::future_status::deferred。那永远也不会等于std::future_status::ready,所以循环永远不会终止。
因此,如果异步执行是必需的,指定std::launch::async发射策略。

使用std::async,GUI线程的响应性也是有问题的,因为调度器没有办法知道哪个线程具有严格的响应性要求。在这种情况下,你可以把std::lanuch::async启动策略传递给std::async,它那可以保证你想要运行的函数马上会在另一个线程中执行。

比起基于线程编程,基于任务的设计能分担你的线程管理之痛,而且它提供了一种很自然的方式,让你检查异步执行函数的结果(即,返回值或异常)。

需要记住的3点:

  • std::thread的API没有提供直接获取异步运行函数返回值的方法,而且,如果这些函数抛出异常,程序会被终止。
  • 基于线程编程需要手动地管理:线程耗尽、oversubscription、负载均衡、适配新平台。
  • 借助默认发射策略的std::async,进行基于任务编程可以解决上面提到的大部分问题。

© 版权声明
THE END
喜欢就支持一下吧
点赞708 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容