C++stop_callback注册停止回调C++20

2026-03-22 18:15:38 1074阅读

C++20 中 std::stop_callback:优雅管理线程取消与资源清理

在并发编程中,如何安全、及时地响应线程取消请求,是保障程序健壮性与资源一致性的关键挑战。C++20 引入了标准化的协作式取消机制(cooperative cancellation),其核心组件之一便是 std::stop_callback。它提供了一种轻量、类型安全且无锁的方式,在 std::stop_source 发出停止请求时自动触发用户定义的清理逻辑。本文将系统解析 std::stop_callback 的设计意图、使用方式、生命周期语义及典型实践模式。

协作式取消:从手动轮询到自动回调

在 C++20 之前,开发者常依赖标志位(如 std::atomic<bool>)配合循环轮询实现“软终止”,但这种方式存在延迟高、语义模糊、易遗漏检查点等问题。C++20 的 std::stop_token/std::stop_source/std::stop_callback元组构建了统一的取消协议:stop_source 是取消信号的发起者;stop_token 是监听者持有的“收据”;而 std::stop_callback 则是绑定于 token 的一次性回调句柄——只要 token 关联的源进入“已停止”状态,回调即被同步执行(在注册线程上下文中)。

值得注意的是,std::stop_callback 并非用于替代异常或中断,而是为确定性资源释放(如关闭文件句柄、解绑事件监听器、归还内存池块)提供精准时机。其构造即注册,析构即注销,语义清晰,无需显式调用 unregister()

基本用法与生命周期管理

std::stop_callback 是一个模板类,接受一个可调用对象(函数对象、lambda 或函数指针)。其构造函数接收 std::stop_token 和该可调用对象;一旦关联的 stop_source 调用 request_stop(),且当前 token 处于可停止状态,回调将在注册时所在线程中立即执行(非新线程)。

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

void example_basic_usage() {
    std::jthread worker([](std::stop_token stoken) {
        // 注册回调:当线程被请求停止时,打印日志并清理
        std::stop_callback callback(stoken, []() {
            std::cout << "[INFO] 清理资源:关闭日志缓冲区\n";
        });

        // 模拟工作循环,定期检查是否应退出
        while (!stoken.stop_requested()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::cout << "[WORK] 执行任务中...\n";
        }
        std::cout << "[INFO] 工作线程正常退出\n";
    });

    // 主线程等待片刻后请求停止
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // jthread 析构时自动 request_stop(),此处显式调用仅作演示
    // worker.request_stop(); 

    // 等待 worker 结束
    worker.join();
}

关键点在于:回调对象 callback 的生存期决定了注册的有效期。一旦 callback 析构(如作用域结束),其注册即被自动移除,后续即使 stop_source 触发,也不会再调用该回调。这避免了悬挂指针与重复执行风险。

避免常见陷阱:拷贝、移动与线程安全

std::stop_callback 不可拷贝(删除了拷贝构造与赋值),仅支持移动语义。这意味着它通常作为局部变量或成员变量持有,不应尝试存储于容器中(除非使用 std::move 后插入)。此外,其内部实现保证了注册/注销操作的线程安全性——多个线程可并发注册不同回调至同一 token,互不干扰。

struct ResourceGuard {
    explicit ResourceGuard(std::stop_token stoken)
        : m_token(stoken)
        , m_callback(stoken, [this]() { cleanup(); }) {}

    // 移动构造合法,回调随对象转移
    ResourceGuard(ResourceGuard&& other) noexcept
        : m_token(other.m_token)
        , m_callback(std::move(other.m_callback)) {}

private:
    void cleanup() {
        std::cout << "[CLEANUP] 释放专属资源\n";
    }

    std::stop_token m_token;
    std::stop_callback m_callback; // 成员变量确保生命周期可控
};

若需动态管理多个回调,推荐将 std::stop_callback 存储于 std::vector<std::stop_callback> 中,并在需要时 std::move容器——但务必注意:移动后原对象处于有效但未指定状态,不可再访问。

实际场景:异步 I/O 与 RAII 封装

在异步网络服务中,stop_callback 可与 std::jthread 深度协同,实现连接级资源自动回收:

#include <memory>
#include <mutex>

class ConnectionHandler {
public:
    ConnectionHandler(std::stop_token stoken, int conn_id)
        : m_conn_id(conn_id)
        , m_stoken(stoken)
        , m_callback(stoken, [this]() { close_socket(); }) {}

private:
    void close_socket() {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (m_socket_fd != -1) {
            ::close(m_socket_fd);
            m_socket_fd = -1;
            std::cout << "[CONN " << m_conn_id << "] 套接字已关闭\n";
        }
    }

    const int m_conn_id;
    std::stop_token m_stoken;
    std::mutex m_mutex;
    int m_socket_fd = -1;
    std::stop_callback m_callback;
};

此例中,ConnectionHandler 对象的析构会自动销毁 m_callback,从而解除对 socket 的清理绑定;而 stop_callback 的存在,又确保了即使 handler 对象因异常提前析构,只要 stoken 已请求停止,socket 仍能被及时关闭。

结语:拥抱标准化的取消范式

std::stop_callback 并非万能开关,而是 C++20 协作式取消生态中不可或缺的“钩子”。它以零成本抽象、确定性执行和 RAII 友好性,显著降低了并发资源管理的复杂度。开发者应摒弃裸标志轮询,转而将清理逻辑封装为 stop_callback,并与 std::jthreadstd::condition_variable 等现代设施组合使用。当每个资源都拥有明确的“停止时行为”,程序的可维护性、可观测性与可靠性将得到本质提升。在 C++20 及更高标准普及的今天,掌握 std::stop_callback 已成为编写稳健并发代码的基本功。

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

目录[+]