《Modern Effective C++》学习笔记7 并发API

条款三十五:优先选用基于任务而非基于线程的程序设计

基于任务?

 auto fut = std::async(doAsyncWork);

基于线程?

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

基于线程vs基于任务

  1. 基于线程未提供获取返回值的途径,而且如果函数抛出异常,程序会终止;

  2. 基于线程的程序要求手动管理线程耗尽,超订,负载均衡以及新平台适配等种种;

  3. 基于任务拥有默认策略,能够避免以上这些问题;

基于线程的应用场景:

  1. 需要访问底层线程实现API,可以通过native_handle成员函数访问;

  2. 你需要且有能力为你的应用优化线程用法,对于个人能力和业务了解有一定的要求;

  3. 你需要实现超越C++并发API的线程技术,例如线程池;

条款三十六:如果异步是需要的,则指定std::launch::async

单纯std::async只是执行任务的默认启动策略,而默认启动策略既允许任务以异步方式进行,也允许任务以同步方式执行。这样忙的时候有可能永远不执行。执行是否立刻可能会导致thread_local变量的不确定性,因为不同时间的thread_local变量可能有变化,也可能导致超时。

为了解决这个问题,如果明确是异步的,则把std::async的第一个参数设为std::launch::async

 auto fut = std::async(std::launch::async, f);
 ​
 // 自动使用std::launch::async的封装函数
 // C++11
 template<typename F, typename... Ts>
 inline
 std::future<typename  std::result_of<F(Ts...)>::type>
 reallyAsync(F&& f, Ts&&... params) {
     return std::async(std::launch,::async,
                       std::forward<F>(f),
                       std::forward<Ts>(params)...);
 }
 ​
 // C++14
 template<typename F, typename... Ts>
 inline
 auto
 reallyAsync(F&& f, Ts&&... params) {
     return std::async(std::launch,::async,
                       std::forward<F>(f),
                       std::forward<Ts>(params)...);
 }

条款三十七:使std::thread型别对象在所有路径皆不可联结

可联结?

其实就是joinable。可联结的std::thread对应底层以异步方式已运行或可运行的线程。不管是处于阻塞,等待调度或者已运行至结束,都是可联结的。

为什么要保证std::thread在所有路径皆不可join?

当函数异常结束时,跳出了正常thread的join路径后会调用函数的析构函数,对于一个可join的thread调用异构函数会导致程序终止,这是规定。

例如下面这个函数:

 void test1() {
   int num = 0;
   auto sum = [&num]() { ++num; };
   std::thread t(sum);
   // 跳过了正常的join流程
   if (false) {
     t.join();、
     return;
   }
   
   return;
 }

如何保证在所有路径皆不可join?

采用RAII技术,进行thread的封装,见下面代码:

 class ThreadRAII {
 public:
   enum class DtorAction ( join, detach );
 ​
   ThreadRAII(std::thread&& t, DtorAction a)
   // 类成员初始化列表要求匹配变量声明顺序
   : action(a), t(std::move(t)) {}
 ​
   ~ThreadRAII() {
     if (t.joinable()) {
       if (action == DtorAction::join) {
         t.join();
       } else {
         t.detach();
       }
     }
   }
 ​
   ThreadRAII(ThreadRAII&&) = default;
   ThreadRAII operator=(ThreadRAII&&) = default;
 ​
   std::thread& get() { return t; }
 private:
   DtorAction action;
   // thread初始化好之后可能就马上要运行函数,最好放在最后声明
   std::thread t;
 };

经过封装修改后的代码就可正常运行:

 void test2() {
   int num = 0;
   auto sum = [&num]() { ++num; };
   ThreadRAII t(
     std::thread(sum),
     ThreadRAII::DtorAction::join
   );
   
   if (false) {
     t.get().join();
   }
   
   return;
 }

注意:在析构时调用join可能导致难以调试的性能异常,调用detach可能导致难以调试的未定义行为。

条款三十八:对变化多端的线程句柄析构函数行为保持关注

什么是线程句柄?

joinable的线程对应着一个底层系统执行线程,而std::future与系统线程也有类似关系,所以std::thread和std::future都可以视作系统线程的句柄。

std::future的析构

std::future的析构函数在正常情况下,只会析构std::future的成员变量。

std::async异步执行时,需要有个地方保存运行结果,这个结果并不在std::future对象中,也不再运行函数中的对象中,通常是堆上的对象,称作共享状态,会有多个std::future关联到这个共享状态,而最后一个std::future就需要阻塞直到异步运行的任务结束(为了同时去析构这个对象?),相当于对std::async运行的任务实施了一次join。

例外析构的条件如下:

  1. std::future关联的共享状态时由于调用了std::async才创建的(例如不是通过std::packaged_task);

  2. 该任务的启动策略是std::launch::async,可以是运行时系统的选择,也可以是调用时显式指定的;

  3. 该std::future是关联到该共享状态的最后一个std::future;

条款三十九:考虑针对一次性事件通信使用以void为模板型别实参的期值

对于事件通信的解决方案:

  1. 条件变量+互斥量,如果两个线程没有临界资源,互斥量的存在显得多余,而且条件变量等待代码可能会被虚假唤醒,此时需要去校验条件是否已经满足;

  2. 使用标志位避免了互斥量的使用,也避免了虚假唤醒的问题,但是这是基于轮询的机制而非基于通知机制;

  3. 条件变量+标志位,条件变量通知了还需要去检查标志位,有点多余;

  4. std::promise+std::future或std::shared_future,此方案的问题在于promise只能设置一次(貌似可以考虑不用void来实现多次),而且共状态需要使用堆内存,参考以下代码;

 std::promise<void> p;   // 信道
 ​
 // 通知方
 p.set_value();
 ​
 // 被通知方
 p.get_future().wait();

条款四十:对并发使用std::atomic,对特种内存使用volatile

std::atomic的操作是原子的,它会对代码可以如何重新排序施加限制,用于多线程访问的数据,且不用互斥量,是并发软件的好工具。

volatile用于冗余读写操作不可以被优化掉的内存,是面对特种内存的好工具。

注意:两个可以混合一起用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值