Item37:使thread在所有路径最后都不可结合
std::thread 的可结合 / 不可结合状态本质
std::thread 对象仅有两种状态,其中可结合状态是风险点:
- 可结合(joinable):对应 “正在运行 / 待调度 / 已结束但未执行
join/detach” 的底层线程,此状态下析构std::thread会直接导致程序终止; - 不可结合(unjoinable):安全状态,包括默认构造、已移动、已
join、已detach的std::thread。
可结合线程析构的致命问题(默认处理方式的弊端)
标准委员会拒绝为 std::thread 析构提供默认处理(隐式 join/detach),因为两种方式均存在严重问题:
弊端 1:隐式 join(等待线程完成)
会引发反直觉的性能异常(比如条件不满足时,仍等待耗时的过滤逻辑执行完毕),调试难度大;
弊端 2:隐式 detach(分离线程)
会导致未定义行为:若线程捕获了函数局部变量,函数返回后局部变量销毁,线程仍访问已释放的栈内存,造成内存篡改,调试成本极高。
解决问题:RAII 封装 std::thread(覆盖所有执行路径)
要确保 std::thread 在所有执行路径(正常返回、异常、break/goto 等)下析构时不可结合,最通用的方案是RAII(资源获取即初始化):将 “确保不可结合” 的逻辑封装到局部对象的析构函数中(析构函数会自动执行,覆盖所有路径)。
原理:RAII 类的设计逻辑
- 构造时接收
std::thread右值(std::thread不可复制,只能移动),并指定析构动作(join/detach); - 析构时先检查线程是否可结合(避免对不可结合线程调用
join/detach导致未定义行为),再执行指定动作; - 提供
get ()方法访问内部std::thread,兼容原生接口(如设置线程优先级)。
解决方案
方案 1:实现 ThreadRAII 类(封装)
1 | class ThreadRAII { |
方案 2:业务函数中使用 ThreadRAII(覆盖所有路径)
1 | bool doWork(std::function<bool(int)> filter, int maxVal = 10'000'000) { |
总结
- std::thread 的可结合状态是高危状态,必须确保所有执行路径下析构时均为不可结合(否则程序终止);
- 隐式 join/detach 均有严重弊端,推荐通过 RAII 封装 std::thread(ThreadRAII 类)实现安全析构;
- 类成员声明时 std::thread 应最后声明(确保其他成员初始化完成后线程启动),且 RAII 类需支持移动、析构前检查可结合性。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 肖恩的博客!
评论
