std::jthread 与停止令牌¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 3/5 | final | B | 2 条(manual 2) | 单审(zyuhao7) | 2026-10-26 |
编译器支持:GCC 10+ · Clang 20+ · MSVC 19.28+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 std::jthread 启动后台 COM 线程,并在 lambda 里轮询 stop_token | ev-gw2radial-jthread-com-thread-001 |
| 把 std::jthread 保存在类成员中,由显式 reset 触发停止与 join | ev-gw2radial-jthread-member-002 |
本站证据页:ev-gw2radial-jthread-com-thread-001 · ev-gw2radial-jthread-member-002
解决什么问题¶
std::thread 析构时若仍可 join,进程会直接终止;忘记 join 或 detach 是常见的崩溃来源。线程之间的停止也一直靠手工约定:额外维护一个 atomic<bool>,所有阻塞点都得自己检查,调用方还要负责通知条件变量并等待退出。
std::jthread 把两件事合并进类型:析构函数先请求停止,再 join;停止请求通过标准的 std::stop_source/std::stop_token 协议传播。线程函数如果能接收第一个 std::stop_token 参数,jthread 会自动把自己的令牌传进去。取消仍是协作式的——令牌只表达“该停了”,不会强行终止线程。
基础用法¶
带 stop_token 参数的可调用对象会收到线程自带的令牌;析构时 jthread 请求停止并等待线程结束。
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <stop_token>
#include <thread>
int main() {
std::mutex mutex;
std::condition_variable_any ready;
std::jthread worker([&](std::stop_token token) {
std::unique_lock lock(mutex);
ready.wait(lock, token, [] { return false; });
});
worker.request_stop();
ready.notify_all();
std::cout << std::boolalpha << worker.joinable() << '\n';
}
输出 true:request_stop() 之后线程尚未被 join,joinable() 仍为真;离开作用域时 jthread 的析构函数完成 join。完整示例没有对应的 examples/ 文件,因为仓库的单文件流程只编译不运行并发代码。
condition_variable_any 提供接收 stop_token 的 wait 重载,等待期间收到停止请求会直接返回,不需要额外的唤醒标志;普通的 std::condition_variable 没有这个重载。
进阶用法¶
stop_source 持有停止状态,stop_token 观察它。jthread 内部各有一个 stop_source,通过 get_stop_source() 和 get_stop_token() 暴露;也可以把外部 stop_source 的令牌传进线程函数,让多个线程共享同一取消信号。
request_stop() 返回布尔值:第一次请求返回 true,重复请求返回 false。stop_requested() 查询状态,stop_possible() 判断是否还可能收到停止请求(没有关联 stop_source 时为假)。
std::stop_callback 在构造时注册回调,停止请求发生时同步执行。回调运行在调用 request_stop() 的线程上,因此不能阻塞,也不应抛异常。若注册时停止已经请求,回调会在构造函数里立即执行。
阻塞点需要真正可中断。condition_variable_any 的 stop_token 重载、std::stop_callback 配合阻塞 I/O 或 poll 的超时,都属于协作式取消;裸 read、recv、sleep_for 不会因为令牌被请求而返回。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| GW2Radial | src/Core.cpp#L121-L133 |
make_unique<std::jthread>,lambda 接收 stop_token,循环检查 stop_requested() |
后台线程的创建、取消条件与自动 join 集中在一处 |
| GW2Radial | include/Core.h#L93 |
std::unique_ptr<std::jthread> comThread_; 作为类成员 |
用指针控制销毁时机,避免线程晚于依赖对象结束 |
两条证据均为 manual 级:作者复核,尚未引入第二名审核者。项目在 InnerShutdown() 中显式 comThread_.reset(),先请求停止并 join,再清理其他成员;这比依赖成员析构顺序更明确。
陷阱与边界¶
取消是协作式的。 jthread 不会杀死线程,只会设置停止状态并等待。线程若从不检查令牌、也不使用可中断的等待原语,析构函数会一直阻塞到线程自然结束。
析构函数可能长时间阻塞。 析构里的 join 没有超时。需要限时退出时,必须在外部逻辑中先请求停止并等待,确认线程结束后再销毁 jthread。
停止回调在请求线程上同步执行。 回调里做重活或再次调用可能阻塞的操作,会拖住调用 request_stop() 的线程;回调抛异常会直接传播到请求方。
成员声明顺序会影响安全。 成员按声明的逆序析构。若 jthread 声明在互斥量、条件变量之前,它会最后才被销毁,线程可能在依赖对象已经析构后继续运行。要么把线程成员声明在依赖对象之后,要么像 GW2Radial 一样在清理函数里显式 reset()。
不接收 stop_token 的函数不会获得令牌。 jthread 只在可调用对象能以 stop_token 为首参调用时传入令牌,否则按无参调用;此时 request_stop() 仍可用,但线程函数拿不到令牌。需要取消时,应由外部通过 jthread::get_stop_token() 取出令牌并传入,或自行实现停止标志。
Clang 支持较晚。 cppreference 将 Clang 17 标为部分支持、Clang 20 标为完整支持;以 Clang 为目标的项目要先用最小示例验证 stop_token 相关库设施。
不能强制终止。 需要硬取消时只能依赖平台 API 或进程隔离;jthread 的模型刻意不提供强制终止。
横向对比¶
| 方案 | 自动 join | 标准取消协议 | 主要代价 |
|---|---|---|---|
std::thread + atomic<bool> |
否 | 手写 | 忘记 join 会终止进程,停止逻辑分散 |
std::jthread |
是 | stop_token/stop_source |
析构可能阻塞,取消仍需协作 |
std::async |
future 析构时可能阻塞 | 无 | 取消能力弱,线程归属不明确 |
| 线程池 | 由池管理 | 各库自定义 | 需要额外依赖,任务粒度受池设计限制 |
跨标准演进:C++20 通过 P0660R10 引入 std::jthread、std::stop_token、std::stop_source 与 std::stop_callback;它们把“join + 协作取消”固化成标准接口,但没有改变协作式取消的基本约束。