• Item 37: Make std::threads unjoinable on all paths.


    Item 37: Make std::threads unjoinable on all paths.

    每个 std::thread 只会处于两种状态状态之一:其一为 joinable,其二为 unjoinable 。一个 joinablestd::thread 对应于一个正在或可能在运行的底层线程。例如,一个对应于处于阻塞或者等待调度的底层线程的 std::threadjoinable。对应于底层线程的 std::thread 已经执行完成也可以被认为是 joinable

    unjoinable 的线程包括:

    • 默认构造的 std::thread。这样的 std::thread 没有执行函数,也就不会对应一个底层的执行线程。
    • std::thread 对象已经被 move。其底层线程已经被绑定到其它 std::thread
    • std::thread 已经 join。已经 join 的对应 std::thread 的底层线程已经运行结束。
    • std::thread 已经 detach。已经 detachstd::thread 与其对应的底层线程已经没有关系了。

    std::threadjoinabilty 状态之所以重要的原因之一是:一个 joinable 状态的 std::thread 对象的析构函数的调用会导致正在运行程序停止运行。例如,我们有一个 doWork 函数,它接收一个过滤函数 filter 和一个最大值 MaxVal 作为参数。 doWork 检查并确定所有条件满足时,对 0 到 MaxVal 执行 filter。对于这样的场景,一般会选择基于任务的方式来实现,但是由于需要使用线程的 handle 设置任务的优先级,只能使用基于线程的方法来实现(相关讨论可以参见 Item 35: Prefer task-based programming to thread-based.)。可能的实现如下:

    constexpr auto tenMillion = 10000000; // see Item 15 for constexpr
    bool doWork(std::function<bool(int)> filter, // returns whether
                int maxVal = tenMillion)         // computation was
    {                                            // performed; see
                                                 // Item 2 for
                                                 // std::function
      std::vector<int> goodVals;  // values that
                                  // satisfy filter
      std::thread t([&filter, maxVal, &goodVals]  // populate
                    {                             // goodVals
                      for (auto i = 0; i <= maxVal; ++i)
                      { if (filter(i)) goodVals.push_back(i); }
                    });
      auto nh = t.native_handle();      // use t's native// handle to set
                                        // t's priority
      if (conditionsAreSatisfied()) {
        t.join();                       // let t finish
        performComputation(goodVals);
        return true;                    // computation was
      }                                 // performed
      return false;                     // computation was
    }                                   // not performed
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    对于上面的实现,如果 conditionsAreSatisfied() 返回 true,没有问题。如果 conditionsAreSatisfied() 返回 false 或抛出异常,std::thread 对象处于 joinable 状态,并且其析构函数将被调用,会导致执行程序停止运行。

    你可能会疑惑为什么 std::thread 的析构函数会有这样的行为,那是因为其他两种选项可能更加糟糕:

    • 隐式的 join。析构函数调用时,隐式去调用 join 等待线程结束。这听起来似乎很合理,但会导致性能异常,并且这有点反直觉,因为 conditionsAreSatisfied() 返回 false 时,也即条件不满足时,还在等待 filter 计算完成。
    • 隐式 detach。析构函数调用时,隐式调用 detach 分离线程。doWork 可以快速返回,但可能导致 bug。因为 doWork 结束后,其内部的 goodVals 会被释放,但线程还在运行,并且访问 goodVals ,将导致程序崩溃。

    由于 joinable 的线程会导致严重的后果,因此标准委员会决定禁止这样的事情发生(通过让程序停止运行的方式)。这就需要程序员确保 std::thread 对象在离开其定义的作用域的所有路径上都是 unjoinable 。但是想要覆盖所有的路径并非易事,return、continue、goto、break 或者异常等都能跳出作用域。

    无论何时,想在出作用域的路径上执行某个动作,常用的方法是将这个动作放入到一个局部对象的析构函数中。这种对象被成为 RAII(Resource Acquisition Is Initialization)对象,产生这个对象的类是 RAII 类。RAII 类在标准库中很常见,例如 STL 容器(每个容器的析构函数销毁容器中的内容并释放它的内存)中的智能指针(std::unique_ptr 析构函数调用它的 deleter 删除它指向的对象,std::shared_ptrstd::weak_ptr 的析构函数中会减少引用计数)、std::fstream 对象(析构函数关闭相应的文件)。但是 std::thread 对象没有标准的 RAII 类,这可能是标准委员会拒绝将 joindetach 作为默认选项,因为他们也不知道这个类应该有什么样的行为。

    好在实现这样的一个类也并非难事。例如,你可以让用户指定 ThreadRAII 类在销毁时选择 join 还是 detach

    class ThreadRAII {
    public:
      enum class DtorAction { join, detach };    // see Item 10 for
                                                 // enum class info
      ThreadRAII(std::thread&& t, DtorAction a)  // in dtor, take
      : action(a), t(std::move(t)) {}            // action a on t
    
      ~ThreadRAII()
      {
        if (t.joinable()) {                     // see below for
                                                // joinability test
          if (action == DtorAction::join) {
            t.join();
          } else {
            t.detach();
          }
        }
      }
      
      std::thread& get() { return t; }         // see below
    
    private:
      DtorAction action;
      std::thread t;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    关于上面代码的几点说明:

    • 构造函数只接收 std::thread 的右值,因为 std::thread 不可拷贝。
    • 构造函数参数排列顺序符合调用者的直觉(std:thread 为第一个参数,DtorAction 为第二个参数),但是成员变量的初始化符合成员变量的申明顺序。在这个类中两个成员变量的前后顺序没有意义,但是通常而言,一个成员的初始化依赖另一个成员。
    • ThreadRAII 提供了 get 函数,用于访问底层的 std::thread 对象。提供 get 方法访问 std::thread,避免了重复实现所有 std::thread 的接口。
    • ThreadRAII 的析构函数首先检查 t 是否为 joinable 是必要的,因为对一个 unjoinable 的线程调用 joindetach 将产生未定义的行为。

    ThreadRAII 应用于 doWork 的例子上:

    bool doWork(std::function<bool(int)> filter,
                int maxVal = tenMillion)
    {
      std::vector<int> goodVals;
      
      ThreadRAII t(                           // use RAII object
        std::thread([&filter, maxVal, &goodVals]
                    {
                      for (auto i = 0; i <= maxVal; ++i)
                        { if (filter(i)) goodVals.push_back(i); }
                    }),
        ThreadRAII::DtorAction::join          // RAII action
      );
    
      auto nh = t.get().native_handle();
      ...
      if (conditionsAreSatisfied()) {
        t.get().join();
        performComputation(goodVals);
        return true;
      }
      return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这个例子中,我们选择 join 作为 ThreadRAII 析构函数的动作。正如前文所述,detach 可能导致程序崩溃,join 可能导致性能异常。两害取其轻,性能异常相对可以接受。

    正如 Item 17: Understand special member function generation. 所介绍的,由于 ThreadRAII 自定义了析构函数,编译器将不在自动生成移动操作,但没有理由让 ThreadRAII 对象不支持移动。因而,需要我们将移动操作标记为 default

    class ThreadRAII {
    public:
      enum class DtorAction { join, detach };
      
      ThreadRAII(std::thread&& t, DtorAction a)
      : action(a), t(std::move(t)) {}
    
      ~ThreadRAII()
      {
        ...  // as before
      } 
    
      ThreadRAII(ThreadRAII&&) = default;            // support
      ThreadRAII& operator=(ThreadRAII&&) = default; // moving
    
      std::thread& get() { return t; }               // as before
      
    private:
      DtorAction action;
      std::thread t;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    至此,本文结束。

    参考:

  • 相关阅读:
    Android日历提醒增删改查事件、添加天数不对问题
    基于c++的简易web服务器搭建(初尝socket编程)
    ★C语言期末课程设计★——学生成绩管理系统(完整项目+源代码+详细注释)
    互联网那些技术 | 扒一扒互联网Markdown的那些事儿
    使用javascript模拟并行位全加法器
    调用Visual Studio的cl.exe编译C/C++程序
    西门子触摸屏上电显示初始画面几秒后,自动切换到下一个画面的具体方法
    win10系统配置vmware网络NAT模式
    一文聊透数字化转型,获得企业未来生存的入场券--童亚斋
    2022年的就业前景
  • 原文地址:https://blog.csdn.net/Dong_HFUT/article/details/126195848