Appearance
回调、线程生命周期与异步任务
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() 或建立统一的异常观察机制。