C++stop_token请求停止协作

2026-03-22 19:15:36 263阅读

C++20 stop_token:优雅协作式取消机制详解

在现代C++并发编程中,如何安全、可控地终止正在运行的异步任务,一直是开发者面临的核心挑战之一。粗暴的线程中断(如 std::thread::detach() 或信号强制终止)极易引发资源泄漏、状态不一致甚至未定义行为。C++20 引入了基于协作式取消(cooperative cancellation)的标准化机制——std::stop_tokenstd::stop_sourcestd::stop_callback。其中,stop_token 作为轻量级“停止请求观察者”,是实现可中断异步操作的关键入口。本文将系统解析其设计哲学、使用范式与典型实践。

协作式取消的设计思想

协作式取消强调“请求—响应”而非“强制终止”。调用方通过 stop_source 发出停止请求,被管理的执行单元(如线程协程或任务函数)持续检查关联的 stop_token 状态,并在适当时机主动退出。这种模式确保了资源清理的确定性:析构函数得以正常执行,锁可被释放,内存能被回收,文件句柄可被关闭。

stop_token 本身不可变、无状态,仅提供对底层停止状态的只读视图。它不持有任何资源,可自由拷贝、传递,甚至跨线程共享——这是其高效与安全的基础。

核心组件关系

  • std::stop_source:拥有停止状态的唯一所有者,提供 request_stop() 方法发起请求。
  • std::stop_token:由 stop_source.get_token() 获取,用于轮询或注册回调。
  • std::stop_callback<Callback>:构造时绑定回调函数,当停止请求发生时自动触发(仅一次)。

三者共同构成一个松耦合、低侵入的取消通知链。

基础用法示例

以下是一个带取消支持的长时间循环任务:

#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>

void worker_task(std::stop_token stoken) {
    for (int i = 0; i < 100; ++i) {
        // 每次迭代前检查是否收到停止请求
        if (stoken.stop_requested()) {
            std::cout << "Worker: stopping at iteration " << i << "\n";
            return;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Worker: iteration " << i << "\n";
    }
    std::cout << "Worker: completed all iterations\n";
}

int main() {
    std::stop_source source;
    std::thread t(worker_task, source.get_token());

    // 主线程等待2秒后请求停止
    std::this_thread::sleep_for(std::chrono::seconds(2));
    source.request_stop();

    if (t.joinable()) {
        t.join();
    }
}

该示例展示了最简协作流程:子线程定期轮询 stop_requested(),主线程通过 stop_source 触发全局停止信号。注意 stop_token 通过值传递,符合其轻量设计目标。

使用 stop_callback 实现自动响应

对于需在停止瞬间执行清理逻辑的场景,stop_callback 提供了更优雅的注册机制:

#include <iostream>
#include <thread>
#include <memory>
#include <stop_token>

void worker_with_cleanup(std::stop_token stoken) {
    auto resource = std::make_unique<int[]>(1024); // 模拟动态资源

    // 注册停止回调:自动释放资源
    std::stop_callback callback(stoken, [&resource]() {
        std::cout << "Cleanup: releasing resource\n";
        resource.reset(); // 显式释放
    });

    for (int i = 0; i < 50; ++i) {
        if (stoken.stop_requested()) {
            std::cout << "Worker: early exit at " << i << "\n";
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::stop_source src;
    std::thread t(worker_with_cleanup, src.get_token());

    std::this_thread::sleep_for(std::chrono::milliseconds(800));
    src.request_stop();

    if (t.joinable()) {
        t.join();
    }
}

stop_callback 的生命周期绑定于其所在作用域;一旦 stop_token 关联的停止状态变为 true,回调即被同步调用(在发出 request_stop() 的线程上下文中),确保清理时机可控。

std::jthread 的天然集成

C++20 同时引入了 std::jthread —— 自动 join 的线程封装,其构造函数原生支持 stop_token 参数,使取消集成近乎零成本:

#include <iostream>
#include <thread>
#include <stop_token>

void jthread_worker(std::stop_token stoken) {
    while (!stoken.stop_requested()) {
        std::cout << "Working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
    }
    std::cout << "jthread_worker: stopped gracefully\n";
}

int main() {
    std::jthread jt(jthread_worker); // 自动获取并传递 stop_token

    std::this_thread::sleep_for(std::chrono::seconds(1));
    jt.request_stop(); // jthread 提供便捷接口
}

std::jthread 在析构时会自动调用 request_stop()join(),彻底规避了“忘记 join 导致程序挂起”的常见错误。

注意事项与最佳实践

  • 避免频繁轮询:在高频率循环中,过度调用 stop_requested() 可能影响性能。应结合 std::condition_variablestd::future::wait_until 等阻塞机制,在等待点统一检查。
  • 线程安全保证stop_token::stop_requested() 是无锁且线程安全的,但其返回值仅代表“某一时刻”的快照;业务逻辑仍需确保自身状态一致性。
  • 不可逆性:一旦 request_stop() 被调用,停止状态永久为 true,stop_token 不可重置。如需多次取消/重启,应创建新的 stop_source
  • 异常安全stop_callback 构造可能抛出 std::bad_alloc(若内存不足),应在关键路径中考虑异常处理

结语

std::stop_token 并非万能中断开关,而是 C++20 为并发生态注入的一套清晰契约:它不替代程序员对资源生命周期的责任,却以最小侵入代价赋予任务自我终结的能力。从手动轮询到自动回调,从裸线程到 jthread,这一机制层层抽象,始终服务于一个目标——让异步代码更可预测、更易维护、更贴近现实世界的协作逻辑。掌握 stop_token,是迈向稳健 C++ 并发编程不可或缺的一步。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

目录[+]