C++ 异步编程:用 `weak_from_this` 安全捕获对象生命周期
本文是 C++ 异步回调生命周期安全系列的第二篇。第一篇 介绍了在回调必须执行时,用 shared_ptr 值捕获延长对象生命周期的模式。
上一篇文章讲了如何通过值捕获 shared_ptr 成员,让回调持有对象所有权,确保回调执行时对象不被销毁。但有时候你并不希望回调阻止对象销毁——你希望对象还活着就执行,对象已销毁就静默退出。
这正是 weak_from_this 的用武之地。
问题背景
在基于事件循环(如 Boost.Asio)的异步编程中,常见写法是在回调中访问 this:
timer->async_wait([this](const auto& error) {
this->DoSomething(); // 危险!
});
若回调触发时对象已被销毁,this 就是悬空指针,必然崩溃。
捕获 [self = shared_from_this()] 能防止崩溃,但会引入另一个问题:回调持有对象的强引用,如果 timer 本身是对象的成员,就会形成循环引用,对象永远无法被销毁,造成内存泄漏。
正确的模式是:用 weak_ptr 捕获——对象存活时执行,对象已销毁时安全退出。
解决方案:enable_shared_from_this + weak_from_this
class MyRunner : public std::enable_shared_from_this<MyRunner> {
public:
static std::shared_ptr<MyRunner> Create() {
return std::shared_ptr<MyRunner>(new MyRunner());
}
void StartTimer(boost::asio::deadline_timer& timer) {
timer.async_wait([weak_self = weak_from_this()](const auto& error) {
if (auto self = weak_self.lock()) {
self->DoPeriodicWork();
}
// 对象已销毁 → 静默退出,不崩溃
});
}
private:
MyRunner() = default;
void DoPeriodicWork() { /* ... */ }
};
使用此模式需满足三个条件:
- 目标类继承
std::enable_shared_from_this<T>。 - 对象必须由
shared_ptr管理(通过工厂方法强制约束)。 - 回调中捕获
weak_from_this(),而非this或shared_from_this()。
关键技术点
Lambda 捕获列表初始化(C++14)
[weak_self = weak_from_this()]
这是 C++14 的 init-capture 语法,等价于:
auto weak_self = weak_from_this();
// lambda 按值捕获 weak_self
weak_ptr 不增加引用计数,持有它不会延长对象生命周期。
weak_from_this() vs shared_from_this()
| 方法 | 返回类型 | 引用计数 | 适用场景 |
|---|---|---|---|
shared_from_this() | shared_ptr<T> | +1 | 需要保证对象在回调期间存活 |
weak_from_this()(C++17) | weak_ptr<T> | 不变 | 回调应在对象销毁后安全退出 |
weak_from_this() 是 C++17 的便捷写法,C++14 等价写法为 std::weak_ptr<T>(shared_from_this())。
回调时的安全检查
if (auto self = weak_self.lock()) {
self->DoPeriodicWork();
}
lock() 原子地检查对象是否存活:存活则返回 shared_ptr,在 if 块内保证对象不被销毁;已销毁则返回 nullptr,回调安全退出。
为什么不用其他捕获方式?
| 捕获方式 | 示例 | 问题 |
|---|---|---|
| 裸指针 | [this] | 悬空指针 → 崩溃 |
| 强引用 | [self = shared_from_this()] | 循环引用 → 内存泄漏 |
| 弱引用 | [weak_self = weak_from_this()] | ✅ 安全、无泄漏 |
强引用的问题较隐蔽:若 timer 是对象成员,回调持有对象的 shared_ptr,则对象与 timer 互相持有,引用计数永远不归零,两者均无法被销毁。
Ray 源码实战:PeriodicalRunner
Ray 的 PeriodicalRunner 以固定间隔周期性执行函数,是这一模式的标准实践:
class PeriodicalRunner : public std::enable_shared_from_this<PeriodicalRunner> {
// ...
void DoRunFnPeriodically(std::function<void()> fn,
boost::posix_time::milliseconds period,
std::shared_ptr<boost::asio::deadline_timer> timer) {
timer->async_wait(
[weak_self = weak_from_this(), fn, period, timer](const auto& error) {
if (auto self = weak_self.lock()) {
if (error == boost::asio::error::operation_aborted) return;
fn();
self->DoRunFnPeriodically(fn, period, timer);
}
});
}
};
当外部释放 shared_ptr<PeriodicalRunner> 后,下一次 timer 触发时,weak_self.lock() 返回 nullptr,周期循环自动停止——无需显式取消,不崩溃,不泄漏。
注意 timer 本身也以值捕获(shared_ptr),确保 timer 在最后一次回调完成前不会被销毁。
shared_ptr 与 weak_ptr 捕获如何选择
| 场景 | 捕获方式 | 原因 |
|---|---|---|
| 必须执行的清理逻辑(节点断连、缓冲区刷新) | shared_ptr | 延长生命周期,保证执行 |
| 周期性任务,对象销毁后应自动停止 | weak_ptr | 对象自然销毁,回调安全退出 |
| UI 刷新、best-effort 通知 | weak_ptr | 视图关闭后静默跳过 |
| Timer 是对象成员 | weak_ptr | 避免循环引用 |
总结
- 任何可能在对象销毁后触发的回调,都不能捕获裸
this。 - 捕获
shared_from_this()防止崩溃,但在存在成员 timer 等场景中会造成循环引用。 - 捕获
weak_from_this()是”存活则执行,销毁则退出”的正确模式。 - 回调内用
lock()原子检查对象存活状态,成功后得到的shared_ptr保证回调全程对象不被销毁。 - 工厂方法(
Create()返回shared_ptr)是保证weak_from_this()合法调用的标准约束方式。
参考资料
- Ray 源码:
src/ray/util/periodical_runner.cc - cppreference:
std::enable_shared_from_this - Scott Meyers,《Effective Modern C++》—— 条款 19:用
shared_ptr管理共享所有权资源 - C++ Core Guidelines R.23:优先使用工厂函数