• 本节开始我们进入全书第7章:并发 API

  • 本节太长不看版:使用基于线程的编程方式,你需要自己管理线程的生命周期,考虑线程性能相关的多种问题,而且无法直接获取返回值;而使用基于任务的方式,这些问题全部交由标准库实现解决,而且可以通过 get() 获取返回值。

  • C++11 中,想要异步运行一个函数 doAsyncWork() 有两种基本选择:

    1. 基于线程(thread-based)的方法:创建一个 std::thread,并让它运行 doAsyncWork
      #include <thread>
      ...
      int doAsyncWork() {...}
      
      std::thread t(doAsyncWork);
      t.join();
      
    2. 基于任务(task-based)的方法:将 doAsyncWork 传给 std::async
      #include <future>
      ...
      int doAsyncWork() {...}
      // fut 的类型为 std::future<int>
      auto fut = std::async(doAsyncWork);
      
  • 基于任务方法相比基于线程方法的第一个好处是可以直接获取函数返回值,实现方式是对 std::async 返回的 std::future 调用 get() 函数。基于线程的方式无法直接实现这一点。另外,如果 doAsyncWork 抛出了异常,基于线程的方法会直接(通过调用 std::terminate)终结程序,而基于任务方法会将异常对象代替函数返回值存储在 std::future,调用 get() 时返回它(当然如果不加 try…catch 则会引发该异常)。
    在这里插入图片描述

  • 基于线程方法更主要的问题在于需要考虑很多线程管理相关的细节:

    • 如果创建了超过操作系统所能提供数量的 std::thread,那么将抛出 std::system_error 异常(即使你的 doAsyncWork 可能是 noexcept 的)。

    • 即使没有多到抛出异常的程度,也可能出现 oversubscription 的问题,即比硬件线程多很多的软件线程处于未阻塞(可运行)的状态。此时操作系统的线程调度器一般会使用时间片轮转方式,轮流让这些线程占用一定的硬件时间。然而,一方面线程的上下文切换需要一定的开销;另一方面不同线程访问的可能是不同区域的内存,切换线程初期CPU的cache命中率骤降,需要一个“冷启动”过程(可能还没启动多久就又切换到了别的线程)。总之,过于频繁的线程切换将导致整体性能的大幅下降。(笔者:这点在线程、网络等资源调度中都是相通的)

    • oversubscription 问题很难解决,因为软硬件线程比例的最优值既取决于运行过程中实时的可运行软件线程数(通常是动态变化的,例如一个程序可能一开始处于 I/O 高负载区,可运行线程数较少;后来进入计算高负载区,可运行线程数上升),又取决于硬件的参数和架构,在一个平台上调优的应用并不能保证在所有平台上运行效率都较高。

  • 众所周知,解决问题的最好方法就是把问题交给别人解决(doge),std::async 就是这样的一种工具。使用它,你无需关注线程相关的细节,那些是标准库实现者负责的。

  • 但是,我们仍有必要了解 std::async 的一些规则,Item 36 中将继续讨论。本节给出一个关键信息:类似上面的只传入待执行函数的调用,std::async 不保证会创建新的线程,它可能会把任务在原线程上执行。如果调度器正面临 oversubscription 需要控制线程数量,这样的做法当然是合理的。但是,例如原调用线程是一个 GUI 线程,对响应效率有很高要求,我们必须把这个任务放到其它线程运行,下一个 Item 将告诉你怎么做。

  • 当然,也有一些情况直接使用线程 API 是更加合理的:

    • 你需要访问底层线程实现的 API。C++的并发 API 一般是基于底层平台的线程 API 实现的(如 Linux 的 pthread 和 Windows 线程)。底层 API 通常提供更丰富的控制(例如线程优先级控制)。std::thread 通过 native_handle() 方法提供对底层线程对象的访问,而 std::async 返回的 std::future 没有类似的功能。
    • 你需要并且有能力为你的应用去做线程使用的优化。(笔者比较熟悉的游戏引擎领域就是这样的例子,几乎所有引擎都需要实现一套自己的线程管理,甚至是整个标准库)
    • 你需要在C++并发 API 之上实现线程技术(例如线程池)。

总结

  1. std::thread 没有提供直接获取异步运行的函数的返回值的功能,并且如果函数抛出了异常,程序会直接被终结。
  2. 基于线程的编程需要考虑线程耗尽,oversubscription,负载均衡以及硬件平台适应性等问题。
  3. 通过 std::async 实现的基于任务的编程方式为你解决了以上大部分问题。
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐