《Effective Modern C++》学习笔记 - Item 37: 保证std::thread在所有运行路径上最终unjoinable

  • 每个 std::thread 对象随时处于两种状态之一:joinableunjoinable。处于 joinable 状态意味着 std::thread 对象必须对其内部的线程资源负责,该线程的状态可以是等待执行、正在执行或已经完成执行。而 unjoinbale 状态正相反,具体包括几种情况:
    • 默认构造的 std::thread。它们没有要执行的函数,因此也不会真正持有一个线程。
    • 已经被用于移动构造其它对象的 std::thread。移动操作将其内部持有线程的控制权交给了其它对象,因此它不再负责管理该线程。
    • 已经被 joinstd::thread。进行 join 后,std::thread 对象不再对应其内部已经完成执行的线程。
    • 已经被 detachstd::threaddetach 会切断 std::thread 对象与其内部线程之间的关联。

  • 必须保证所有 std::thread 在最终处于 unjoinable 状态最直接的原因是:如果一个 joinable 的 std::thread 的析构函数被调用,那么程序执行会直接被终结。这样的行为是因为 std::thread 能选择的另外两种行为都很糟糕:

    • 隐式执行 join。这种情况下,std::thread 的析构函数必须等待线程的函数执行完成,这从逻辑上听起来较为合理,但会导致难以追踪的性能异常
    • 隐式执行 detach。这种情况下,std::thread 直接切断自身与线程的连接,线程将继续执行下去。这可能引发更难以debug的问题,例如该线程执行的 lambda 函数从创建 std::threadf 函数中按引用捕获了一个局部变量。一段时间后 f 已经执行完成并返回,其栈帧被弹出,而该线程被与 std::thread 对象切断后继续执行。假设将来某个函数 g 被调用时占用了 f 原来的空间,而线程在执行中改变了其引用变量的值,那么在 g 看来,其栈帧内的内存竟然自行发生了变化!
  • 因此,C++标准禁止了销毁 joinable 线程的行为。

  • 于是正如本节标题所述,我们应保证 std::thread 对象在所有运行路径上最终是 unjoinable 的。这与所有的资源管理问题是类似的,我们不应该手动检查所有的执行路径,而应该使用 RAII,通过任何对象在超出生存范围(scope,体现为大括号)时必定会被调用析构函数的原理做它们的管理。C++标准库中没有对 std::thread 的 RAII 管理类,但我们可以自己动手写一个:

class ThreadRAII
{
public:
    enum class DtorAction { join, detach };

    // 仅允许 std::thread 参数的移动构造,不允许拷贝构造
    // 成员初始化中,由于线程初始化后可能立刻开始执行,一般把它放在最后
    ThreadRAII(std::thread&& t, DtorAction a) : action(a), t(std::move(t)) {}
    
    // 声明析构函数导致 ThreadRAII 的移动构造和移动赋值函数不会被自动生成
    // 实际上默认生成的版本(member-wise 移动 t 和 action)没有问题,因此把它们加回来
    ThreadRAII(ThreadRAII&&) = default;
    ThreadRAII& operator=(ThreadRAII&&) = default;
	
	// get 接口函数
    std::thread& get() { return t; }

    ~ThreadRAII() {
        // 必须先检查,对 unjoinable 的线程进行 join 或 detach 会导致 undefined behavior
        if (t.joinable()) {
            if (action == DtorAction::join) {
                t.join();
            } else {
                t.detach();
            }
        }
    }
private:
    std::thread t;
    DtorAction action;
};

int doAsyncWork() { ... }

ThreadRAII t(std::thread(doAsyncWork), ThreadRAII::DtorAction::join);
// 通过 get 函数访问内部的线程(进一步访问 native_handle 或进行其它操作)
auto nh = t.get().native_handle();

// 假设 t 此时应该被 join
t.get().join();
// 即使不写上句在离开右括号时 t 也会被按构造函数中声明的 DtorAction::join 或 detach 操作

总结

  1. 保证 std::thread 在所有运行路径上最终是 unjoinable 的。
  2. 销毁时 join 的行为可能导致难以debug的性能异常。
  3. 销毁时 detach 的行为可能导致难以debug的 undefined behavior。
  4. std::thread 对象放在成员(初始化)列表的最后。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值