Skip to content

回调、线程生命周期与异步任务

std::function:类型擦除

std::function<R(Args...)> 能以统一接口保存函数、函数对象、Lambda、bind 结果等不同可调用对象:

cpp
std::function<int(int)> square = [](int x) { return x * x; };
int result = square(4);

标准只规定行为,不规定它必须通过虚函数或一定在堆上分配。常见实现会使用类型擦除管理器、间接调用,并对足够小的可调用对象采用小对象优化(SBO)。可能成本包括:构造/复制时的分配,调用时的间接跳转和空对象检查,以及优化器较难内联。

  • 回调类型和调用目标在编译期已知时,优先用模板参数或泛型 Lambda;
  • 需要异构回调、运行期注册或 ABI 稳定接口时,std::function 很合适;
  • 调用空 std::function 会抛 std::bad_function_call,应在接口层定义是否允许为空。

Lambda 与 std::bind

现代 C++ 通常优先使用 Lambda:捕获对象、参数映射和逻辑都更直观,类型推导与诊断也更好。

cpp
// 清晰表达绑定对象、参数和额外逻辑
auto callback = [session, timeout](Request request) {
    return session->send(std::move(request), timeout);
};

std::bind 仍可用于适配旧接口或简单参数重排,但 _1_2 占位符可读性较差;也不要假设 Lambda 必然比 bind 更快,是否内联和是否分配取决于使用上下文与优化器。

回调若会异步保存,引用捕获必须格外小心:

cpp
std::function<void()> bad() {
    std::string text = "hello";
    return [&text] { use(text); }; // 返回后 text 已销毁,悬空引用
}

优先值捕获、移动捕获,或捕获 shared_ptr;同时避免不必要的 shared_ptr 环。

线程安全的回调注册

回调系统的常见策略是:在 mutex 内修改注册表;触发事件时先复制一份回调列表、随后在锁外执行。这样用户回调不会在持锁状态运行,避免死锁和长时间阻塞注册操作。

cpp
using Callback = std::function<void(int)>;

struct Entry {
    int priority;
    Callback callback;
    std::function<bool(int)> filter;
};

void trigger(int value) {
    std::vector<Entry> callbacks;
    {
        std::lock_guard lock(mutex_);
        callbacks = callbacks_; // 复制快照
    }

    std::sort(callbacks.begin(), callbacks.end(),
              [](const Entry& a, const Entry& b) {
                  return a.priority > b.priority;
              });

    for (const auto& entry : callbacks) {
        if ((!entry.filter || entry.filter(value)) && entry.callback) {
            entry.callback(value); // 锁外执行
        }
    }
}

这定义的是“触发时的快照”语义:即使某回调并发注销,本次已复制的快照仍可能执行它。若需要强注销保证、回调对象生命周期管理或可重入控制,应返回订阅 token,并设计引用计数/执行中计数等明确协议。

join()detach()std::jthread

std::thread 对象销毁前必须处于不可 join 状态;若仍 joinable,析构会调用 std::terminate()

  • join():等待线程结束,是默认首选;便于控制资源、异常处理与程序关闭顺序。
  • detach():线程脱离 std::thread 对象独立运行,调用者无法 join、获取结果或可靠地知道它何时结束。

detach() 只适合真正自包含的后台任务:不访问会销毁的局部变量/对象、不依赖进程关闭前完成、不需要返回结果或错误。它不适合“为了不想管理线程”而使用;进程退出时分离线程不保证得到优雅收尾。

C++20 优先考虑 std::jthread:析构会请求停止并 join,配合 std::stop_token 更容易实现可控退出。

std::async 与异常传播

std::async 返回 std::future。任务中抛出的异常被存入共享状态,在调用 future.get() 时于调用线程重新抛出:

cpp
std::future<int> result = std::async(std::launch::async, [] {
    throw std::runtime_error("request failed");
    return 0;
});

try {
    result.get();
} catch (const std::exception& e) {
    report(e);
}

指定 std::launch::async 才要求异步执行;不指定策略时实现可选择延迟执行(deferred),任务可能直到 get()/wait() 才在当前线程运行。future::get() 只能调用一次;只用 wait() 不会重新抛出任务异常,因此关键任务应调用 get() 或建立统一的异常观察机制。

使用 Markdown 与 VitePress 构建