c++20的std::stop_token和std::jthread如何优雅地中断线程? (协作式取消)

std::jthread构造时自动绑定stop_token,线程函数直接接收std::stop_token参数即可;析构时自动request_stop()并join(),但需线程函数主动轮询stop_requested()以响应取消。

std::jthread 构造时自动绑定 stop_token,无需手动传递

std::jthread 在构造时会自动生成一个 std::stop_source,并把对应的 std::stop_token 传给线程函数——这是它比 std::thread 更“优雅”的起点。你不用显式创建 std::stop_source,也不用担心忘记把 token 传进去。

常见错误是仍像用 std::thread 那样手动管理 token,结果重复构造或丢失绑定:

  • 误写 std::jthread t{[](std::stop_token){});:虽然能编译,但丢掉了自动绑定的语义,token 实际来自 jthread 内部,不是你传的那个
  • 试图在构造后调用 t.get_stop_source().request_stop() 前,先用 t.get_stop_token() 去轮询——可行,但没必要,因为线程函数已天然持有有效 token

正确做法就是直接让线程函数接受 std::stop_token 参数:

std::jthread t{[](std::stop_token st) {
    while (!st.stop_requested()) {
        // 做工作...
        std::this_thread::sleep_for(100ms);
    }
    // 自动调用 request_stop() 并 join()
}};

在循环中用 stop_token::stop_requested() 检查取消请求

std::stop_token::stop_requested() 是轻量、无锁、线程安全的检查方式,适合高频轮询。它不阻塞,也不抛异常,只返回 bool,符合协作式取消“主动让出”的设计哲学。

注意几个关键点:

  • 不要用 st.stop_possible() 判断是否能取消——它只表示 token 是否关联了 source,几乎总是 true,对逻辑无实质帮助
  • 避免在 long-running 系统调用(如 read(), accept())中只靠 stop_requested() 轮询,会造成 busy-wait;应配合可中断的等待机制(见下一条)
  • 如果线程正在等待某个条件变量,需在 waitwait_for 中传入 token,否则可能错过取消信号

例如,用 std::condition_variable::wait_until + std::stop_token 实现可中断等待:

std::jthread t{[](std::stop_token st) {
    std::mutex m;
    std::condition_variable cv;
    bool ready = false;

    std::unique_lock lk{m};
    while (!ready && !st.stop_requested()) {
        if (cv.wait_until(lk, std::chrono::steady_clock::now() + 1s, [&]{ return ready; })) {
            break;
        }
    }
}};

std::jthread 析构时自动 request_stop() + join(),但需确保线程函数响应 token

这是 std::jthread 最省心的特性:离开作用域时,它会自动调用内部 stop_source.request_stop(),然后阻塞等待线程退出。但前提是——你的线程函数必须定期检查 stop_token,否则它会永远卡在某处,导致析构 hang 住。

典型陷阱:

  • 线程函数里没检查 token,或者只在循环开头检查一次,之后就进入不可中断的长耗时操作(如大数组排序、文件读取)
  • 使用了不支持 cancellation 的第三方库调用(如某些 C 风格网络 API),且未设超时或封装中断逻辑
  • 在持有锁时长时间等待,而锁未被设计为可被中断

解决思路不是“避免析构”,而是把取消点拆得足够细。例如,在处理大量数据时插入检查:

std::jthread t{[](std::stop_token st) {
    for (int i = 0; i < 1000000; ++i) {
        process_item(i);
        if ((i % 1000 == 0) && st.stop_requested()) {
            break;
        }
    }
}};

需要外部触发取消?用 get_stop_source() 获取可写句柄

当线程启动后,你想从其他地方(比如主线程、信号处理器、UI 事件)主动请求停止,就得拿到它的 std::stop_source。jthread 提供 get_stop_source() 成员函数,返回一个可拷贝、可移动的句柄。

注意事项:

  • get_stop_source() 返回的是副本,调用 request_stop() 是线程安全的,但多次调用无副作用(只会生效第一次)
  • 不能跨线程保存 std::stop_token 后再拿去调用 request_stop()——token 只读,只有 source 才能发起请求
  • 如果你需要多个线程监听同一取消信号,应共享同一个 std::stop_source,而非多个 jthread 各自独立

示例:主线程在特定条件下提前终止工作线程:

std::jthread worker{[](std::stop_token st) {
    while (!st.stop_requested()) {
        do_work();
        std::this_thread::sleep_for(

50ms); } }}; // ... 一段时间后 if (some_condition) { worker.get_stop_source().request_stop(); // 安全,非阻塞 }
协作式取消真正难的不是 API 调用,而是在线程执行路径的关键节点插入恰当的检查点——尤其是那些看似“短暂”实则可能因数据规模或系统负载而变长的操作。token 本身不会中断任何东西,它只提供一个“该停了”的信号;谁来响应、何时响应、响应后如何清理,全在你自己手里。