C++20 detach后的std::jthread用std::stop_token停止是否安全?
std::jthread 相关问题解答 在C20标准中,std::jthread作为比std::thread更安全的线程类被引入,其会在线程对象析构时自动调用停止请求并join对应线程,完成相关资源清理。
同时C20引入了协作式取消机制:每个std::jthread会内部管理一个std::stop_source对象维护底层线程的停止状态,该std::stop_source对外暴露std::stop_token,外部代码可通过该token安全读取线程的停止状态。
用户提供的实现代码如下:
class foo { std::stop_token stok; std::stop_source ssource; public: void start_foo() { // ... auto calculation = [this](std::stop_token inner_tok) { // ... (*this is used here) while(!inner_tok.stop_requested()) { // stuff } } auto thread = std::jthread(calculation); ctok = thread.get_stop_token(); ssource = thread.get_stop_source(); thread.detach(); // ?? } void stop_foo() { if (ssource.stop_possible()) { ssource.request_stop(); } } ~foo() { stop_foo(); } }
注意:
foo类由std::shared_ptr管理,且没有公开构造函数。实际运行场景中,其他线程可能对已经detach的线程对应的foo实例调用foo::stop_foo()方法。
针对提出的三个问题,解答如下:
1. 当前写法是否安全?
不安全,核心问题有两点:
- 悬空指针风险:线程函数捕获了
this裸指针,即使foo由shared_ptr管理,detach后的线程运行周期完全不受foo实例生命周期约束,一旦foo实例被销毁,线程内访问*this就会触发未定义行为。在MSVC下测试没有异常只是概率性的正常表现,属于未定义行为的常见情况,不代表代码逻辑正确。 - 安全机制失效风险:
std::jthread的核心安全设计就是析构时自动触发停止、等待线程执行完成,主动调用detach()后该机制直接失效,jthread对象析构时不再等待线程结束,完全退化为std::thread的detach行为,失去了使用jthread的核心价值。另外代码存在变量名笔误,赋值目标ctok未在类成员中声明,类内定义的停止token成员为stok,会直接触发编译错误。
2. detach后线程是否还能接收std::stop_source的停止通知?
可以正常接收。std::stop_source和std::stop_token的停止状态存储在独立的共享状态块中,生命周期和线程句柄是否detach、jthread对象是否销毁没有绑定关系:只要持有的ssource对象本身有效,调用request_stop()后,所有关联的std::stop_token(包括线程函数参数传入的inner_tok)都可以立刻查询到停止请求,不受detach操作影响。
需要注意的是,能收到停止通知不代表逻辑安全,如果线程收到通知后访问已经销毁的foo实例,依然会触发未定义行为。
3. 是否存在更优实现?当前MSVC下跑通的方案是否可移植?
当前方案不具备可移植性:未定义行为的表现和编译器实现、编译优化等级、运行时环境都有关系,在MSVC下正常运行不代表在其他编译器(如GCC、Clang)或其他编译选项下可以正常工作,随时可能出现崩溃、内存损坏等问题。
更合理的实现方案按需选择:
- 方案一(推荐,符合jthread设计初衷):不要调用detach,直接将
std::jthread作为foo的成员变量持有。这种写法不需要手动维护stop_source和stop_token,jthread成员在foo析构时会自动发送停止请求并等待线程执行完成,不需要手动在析构函数中调用停止逻辑,从机制上避免生命周期问题。示例写法:
class foo { std::jthread worker; public: void start_foo() { worker = std::jthread([this](std::stop_token stok) { while(!stok.stop_requested()) { // 业务逻辑 } }); } // 无需手动编写stop_foo、析构停止逻辑,jthread自动完成安全清理 };
- 方案二(极不推荐,仅适用于必须detach的特殊场景):禁止捕获裸
this指针,改为捕获std::weak_ptr<foo>,线程循环执行前先尝试通过weak_ptr锁定获取有效shared_ptr,锁定失败说明foo实例已经销毁,直接退出线程即可,从根源上避免悬空指针访问。
内容的提问来源于stack exchange,提问作者HashAL78

