ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

std::thread 生命周期管理的两个常见陷阱:自我 join 与未处理的 joinable

std::thread 生命周期管理的两个常见陷阱:自我 join 与未处理的 joinable 在多线程编程中std::thread是最基础也最容易出错的工具之一。许多看似偶发的死锁、程序异常终止往往都源于对线程对象生命周期的误解。本文将解析两个的陷阱在线程函数内部调用join()等待自己导致死锁重新赋值或析构一个仍然处于joinable状态的std::thread对象导致程序调用std::terminate异常退出。理解这两个问题能帮助你避免一些线程生命周期管理错误。一、理解 std::thread 的 joinable 状态在进入具体陷阱之前先回顾一个基本概念joinable。一个std::thread对象在以下情况下是joinable的它关联了一个正在运行或已经运行结束但尚未被join()/detach()的线程简单说只要线程对象“拥有”一个底层线程且这个线程还没有被回收它就是joinable的。joinable状态非常重要因为 C 标准规定当一个joinable的std::thread对象被析构时会调用std::terminate导致程序直接终止对一个joinable的std::thread对象使用赋值运算符时同样会调用std::terminate。因此在使用std::thread时必须确保在对象生命周期结束前要么join()要么detach()让它变成非joinable状态。二、陷阱一线程函数内 join 自己导致死锁2.1 问题场景假设我们有一个工作线程类它内部维护一个std::thread成员。外部可以调用start()启动线程调用stop()停止线程。stop()的实现通常是设置一个停止标志然后join()等待线程结束以保证资源被正确回收。#include iostream #include thread #include atomic class Worker { public: Worker() : running_(false) {} void start() { running_ true; thread_ std::thread(Worker::run, this); } void stop() { running_ false; if (thread_.joinable()) { thread_.join(); // 等待线程结束 } } private: void run() { while (running_) { // 模拟工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟某种错误条件触发停止 if (/* 某个错误发生 */) { stop(); // 注意这里在当前线程内部调用了 stop() } } } std::atomicbool running_; std::thread thread_; };乍一看似乎没什么问题错误发生时线程主动调用stop()来停止自己。但运行后你会发现程序会卡住不再响应。2.2 原因分析问题出在stop()中的thread_.join()。调用链如下run()是thread_所代表的线程正在执行的函数在run()内部调用了stop()stop()中执行thread_.join()join()会阻塞调用线程直到thread_所代表的线程执行结束但thread_所代表的线程正是当前正在执行join()的线程于是线程在等待自己结束而它自己又不可能结束因为它在等待join()返回。这形成了一个经典的自我死锁线程自己 join 了自己。2.3 解决方案原则join()只能由创建线程的外部线程调用不能在线程自身执行路径中调用。推荐做法是线程函数只负责根据标志位自行退出join()由外部线程在合适的时机调用。修改后的代码class Worker { public: void start() { running_ true; thread_ std::thread(Worker::run, this); } void stop() { running_ false; // 不在 stop() 中 join由外部线程在安全的地方 join } void join() { if (thread_.joinable()) { thread_.join(); } } private: void run() { while (running_) { // 模拟工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (/* 某个错误发生 */) { // 只设置标志让循环退出不直接调用 join running_ false; break; } } } std::atomicbool running_; std::thread thread_; }; // 使用示例 int main() { Worker worker; worker.start(); // 等待一段时间后停止 std::this_thread::sleep_for(std::chrono::seconds(1)); worker.stop(); // 只设置标志 worker.join(); // 外部线程安全地 join }这样线程在执行完当前循环后自然退出join()由main线程调用不会出现自己等自己的死锁。如果确实需要在线程内部触发停止并立即回收不推荐可以增加线程 ID 检查作为兜底void stop() { running_ false; if (thread_.joinable() thread_.get_id() ! std::this_thread::get_id()) { thread_.join(); } }但这种做法仍然不够优雅建议优先采用“标志退出 外部 join”的模式。三、陷阱二重新赋值或析构未处理 joinable 线程导致 terminate3.1 问题场景另一个常见错误是一个类中有start()方法每次调用都希望启动一个新线程。很多开发者会直接写#include thread #include iostream class Task { public: void start() { // 直接赋值给 thread_期望启动新线程 thread_ std::thread(Task::run, this); } void run() { std::cout Task running\n; std::this_thread::sleep_for(std::chrono::seconds(1)); } private: std::thread thread_; }; int main() { Task task; task.start(); // 第一次启动正常 std::this_thread::sleep_for(std::chrono::milliseconds(100)); task.start(); // 第二次启动程序直接终止 return 0; }运行上面的程序第二次调用start()时程序会直接异常终止甚至不会看到任何错误提示。3.2 原因分析问题在于std::thread的赋值运算符。C 标准规定如果目标std::thread对象是joinable的那么对它进行赋值操作会调用std::terminate。同样如果一个joinable的std::thread对象被析构也会调用std::terminate。在上面的例子中第一次task.start()后thread_关联了一个线程此时它是joinable的第二次task.start()执行thread_ std::thread(...)左侧thread_仍然是joinable状态于是程序调用std::terminate导致异常退出。值得注意的是即使线程已经执行结束但只要没有调用join()或detach()std::thread对象仍然是joinable的同样会触发terminate。很多人误以为线程函数执行完就没事了实际上对象仍然持有底层资源必须显式回收。3.3 解决方案原则在重新赋值或析构std::thread对象之前必须确保它处于非joinable状态。推荐做法是在启动新线程之前先检查并回收旧线程。修改后的代码class Task { public: void start() { // 如果旧线程仍可加入先等待其结束 if (thread_.joinable()) { thread_.join(); } // 现在 thread_ 已经非 joinable可以安全赋值 thread_ std::thread(Task::run, this); } void stop() { if (thread_.joinable()) { thread_.join(); } } void run() { std::cout Task running\n; std::this_thread::sleep_for(std::chrono::seconds(1)); } private: std::thread thread_; };如果不想等待旧线程结束也可以选择detach()但detach()会导致线程脱离控制无法再回收通常不推荐。更安全的做法是使用 RAII 封装或者直接使用 C20 引入的std::jthread它在析构时会自动join并且支持协作式取消能有效减少这类错误。四、总结与最佳实践通过以上两个陷阱我们可以总结出几条线程生命周期管理的重要原则1. 谁创建谁负责回收创建了std::thread对象的代码必须负责在合适的时机调用join()或detach()确保对象在析构或重新赋值前处于非joinable状态。2.join()不能由线程自己调用join()的语义是“等待另一个线程结束”。如果在线程自己的执行路径中调用join()等待自己必然导致死锁。线程应该通过标志位或条件变量自行退出join()由外部线程统一调用。3. 启动新线程前先处理旧线程如果类中有可能多次启动线程每次启动前都要检查thread_.joinable()若为true则先join()或detach()再创建新线程。4. 尽量使用高层抽象C20 的std::jthread自动处理析构时的join避免遗忘也可以自己封装一个 RAII 线程管理类在析构函数中自动join对于需要频繁启停的场景考虑使用线程池避免频繁创建销毁线程。5. 调试建议如果程序出现莫名的terminate或死锁首先检查是否有std::thread对象在joinable状态下被析构或赋值是否在某个线程内部调用了join()等待自己是否忘记在重新启动线程前回收旧线程。
返回列表