Item37 Make std::threads unjoinable on all paths

原创 2017年04月15日 21:31:15

​   每一个std::thread对象都有两种状态,一个是joinable状态,另外一个则是unjoinable状态。状态是joinable状态的std::thread对象其底层对应的是可以运行的线程。这个线程可以是阻塞的或者是等待被调度运行的,也可以是已经运行的。而一个unjoinable状态的std::thread对象可以是如下几种情况:

  • 默认构造的std::thread对象,因为没有指定要运行的函数,所以没有对应要运行的线程。
  • std::thread对象已经被move了,也就是其底层对应的线程已经和其他std::thread对象绑定了。
  • std::thread对象已经被joined了,已经joinedstd::thread其对应的线程已经运行结束了。
  • std::thread对象已经被detached了,一个detachedstd::thread对象与其对应的线程已经没有任何关系了。

​   std::threadjoinableunjoinable的状态很重要,其中一个最重要的原因是当一个状态是joinablestd::thread对象其析构函数被调用的时候会导致执行的程序terminated。例如我们有一个过滤功能的函数doWork,这个函数接收一个过滤函数,和一个最大值MaxVal,然后检查在0到MaxVal范围内附合过滤条件的数值。对于这样的一个功能来说我更倾向于使用基于任务的方式(参见Item35)来实现,但是因为需要设置任务的优先级需要使用原生的handle,所以只能使用std::thread的API来运行,基于任务的API不提供获取原生handle的方法,使用基于线程的方式其实现如下:

constexpr auto tenmillion = 10000000;
bool doWork(std::function<bool(int)> filter, int maxVal = tenmillion) {
  std::vector<int> goodVals;
  std::thread t([&filter, maxVal, &goodVals] {
    for(auto i = 0; i <= maxVal; ++i) {
      if (filter(i)) goodVals.push_back(i);
    }
  });
  auto nh = t.native_handle();
  //...... 设置任务的优先级
  if (conditioanAreSatisfied()) {
    t.join();
    performComputation(goodVals);
    return true;
  }
  return false;
}

​​   在解释上述代码之前,为了让tenmillion的值更易读可以借助于C++14提供的撇号来对其数值进行分割使其更易读,代码如下:

constexpr auto tenMillion = 10'000'000;

​   上面的代码在线程运行后,通过conditioanAreSatisfied来判断条件是否满足,如果满足就开始等待线程结束,然后对过滤后的结果进行计算,最后返回true,如果不满足就直接返回false,但是std::thread的状态是joinable,doWork执行结束后会调用std::thread对象的析构函数,这就会导致程序terminated。

​   你可能会好奇为什么std::thread在析构的时候其行为会是这样的,因为如果是其它的行为的话很明显会更糟糕,其他的行为如下:

  • 隐式的join,析构函数被调用,然后隐式的去调用join方法,等待线程完全执行结束,听起来似乎很合理,但是这会导致性能异常,因为根据conditioanAreSatisfied的结果可知是不符合条件的,理所应当不应该继续等待线程运行结束。
  • 隐式的detach,这种行为下虽然在不满足条件的情况下doWork可以快速的返回,但是这会导致潜在的bug,还有可能导致程序crash掉,因为doWork执行结束了,其内部的栈变量goodVals其内存会被释放,但是线程仍然在运行,其内部会访问内存被释放掉的goodVals,这就会导致程序crash掉。

​   由于一个joinable状态的std::thread对象所造成的后果十分可怕,因此标准委员会实际上禁止这种行为的发生(通过终止程序来禁止这种行为),因此这要求在使用std::thread对象的时候应该在所有的代码执行路径中确保std::thread的状态都是unjoinable的,但是覆盖所有的执行路径并非易事,它包括return、continue、break、goto或异常等能够跳出作用域的情况,这将导致非常多的路径。无论你什么时候想在所有出作用域的地方做某件事,最常见的方法就是将这件事放入到这个局部对象的析构函数中。这些对象被称为RAII对象,他们的对应的类被称为RAII类。RAII表示”资源获取即初始化”,尽管这种技术的关键是析构而非初始化。std::thread并不是一个RAII类,这是因为标准委员会拒绝隐式的joindetach,因为他们也不知道具体该怎么做。

​   幸运的是实现这样的一个RAIIstd::thread类并不困难,下面这段代码就实现了一个RAII的std::thread,并且可以指定其析构的行为。

class ThreadRAII {
  public:
     enum class DtorAction { join, detach };
     ThreadRAII(std::thread&& t, DtorAction a) : action(a), t(std::move(t)){}
     ~ThreadRAII() {
       // 可能存在race condition,t.joinable和具体的join/detach之间存在空隙,std::thread可能会被设置成
       // unjoinable
       if (t.joinable()) {
         if (action == DtorAction::join) {
           t.join();
         } else {
           t.detach();
         }
       }
     }
     std::thread& get() { return t;}
 private:
    DtorAction action;
    std::thread t;
}

虽然上面的代码几本上很清楚了,但是以下几点解释还是很有必要的:

  • 构造函数只接受std::thread的右值引用,因为我们需要将传入的std::thread对象move到ThreadRAII对象中。
  • ThreadRAII提供了一个get成员函数来获取它对应的std::thread对象,这模仿了智能指针的get方法。提供get方法可以避免ThreadRAII实现所有的std::thread的接口。
  • ThreadRAII析构函数触发前先检查了std::thread是否是joinable,这是必要的,因为在unjoinablestd::thread对象上调用joindetach会导致未定义的行为。

通过将ThreadRAII类放入到doWork中来解决其不满足条件时导致的程序terminated,代码如下:

bool doWork(std::function<bool(int)> filter, int maxVal = tenmillion) {
  std::vector<int> goodVals;
  ThreadRII t(std::thread([&filter, maxVal, &goodVals] {
    for(auto i = 0; i <= maxVal; ++i) {
      if (filter(i)) goodVals.push_back(i);
    }
  }), ThreadRAII::DtorAction::join);

  auto nh = t.native_handle();
  //...... 设置任务的优先级
  if (conditioanAreSatisfied()) {
    t.join();
    performComputation(goodVals);
    return true;
  }
  return false;
}

​   在上面的代码中,我们选择了在ThreadRAII中调用join函数等待线程执行结束,因为上文提到使用detach可能会导致程序crash,难以调试。上文中也提到了join会影响到程序的效率。 但是在两者之间做选择,性能下降貌似是最好的选择了。C++11C++14没有实现线程中断的机制,尽管这是可以自己手动实现的。但是这不在本文的讨论范围之内。

​   根据Item 17的解释,因为ThreadRAII声明了析构函数,那么编译器就不再生成默认的移动构造和移动赋值操作符,而ThreadRAII不应该是一个不可移动的类,为此通过显示的声明移动构造和移动赋值操作符使得ThreadRAII具备移动语义,代码如下:

class ThreadRAII {
  public:
     enum class DtorAction { join, detach };
     ThreadRAII(std::thread&& t, DtorAction a) : action(a), t(std::move(t)){}
     ~ThreadRAII() {
       .....
     }
     ThreadRAII(ThreadRAII&&) = default;
     ThreadRAII& operator=(ThreadRAII&&) = default;
     std::thread& get() { return t;}
 private:
    DtorAction action;
    std::thread t;
}
版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

Item37 Make std::threads unjoinable on all paths

这个系列的文章来自于Effective Modern C++的读书笔记,我抽取了其中比较重要的,不容易理解的,平常我们开发过程中也不太在意的一些Item进行分析。 ​ 每一个std::thr...

Effective modern C++ 条款 39:让std::thread在所有路径上不可join(Make std::threads unjoinable on all paths)

每个std::thread对象都处于两种状态之一:joinable和unjoinable。一个joinable的std::thread对象对应一个正在运行或处于可以运行状态的异步执行的线程。例如,一个...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

命令make是如何工作的。[Makefile,Linux]

make是如何工作的

POJ1942——Paths on a Grid

Paths on a Grid Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 24783   Accepted:...

POJ1942-Paths on a Grid

转载请注明出处:優YoU  http://user.qzone.qq.com/289065406/blog/1301543725 大致题意: 给定一个矩形网格的长m和高n,其中m和n都是uns...

Make a website all about you.

一、tags heading first-level header subheading: h2/h2 all the way down to h6/h6 paragraph i...

poj 1942 Paths on a Grid

Paths on a Grid Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 25124 ...

Item25 Use std::move on rvalue reference, std::forward on universal references

这个系列的文章来自于Effective Modern C++的读书笔记,我抽取了其中比较重要的,不容易理解的,平常我们开发过程中也不太在意的一些Item进行分析。 ​ 右值引用只能绑定到那些可以移...

All threads (200) are currently busy解决方案

2011年1月22日高级性能测试人员技能1.    开发知识:n         至少熟练掌握一门面向对象编程语言(例如C#),熟悉2门或者多门语言(具体需要熟悉哪些语言,可以根据项目情况在...

make命令与makefile文件

一、多个源文件带来的问题在编写c/c++测试程序时,我们习惯每次修改一处代码,然后就马上编译运行来查看运行的结果。这种编译方式对于小程序来说是没有多大问题的,可对于大型程序来说,由于包含了大量的源文件...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)