yuqi-zheng

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() { /* ... */ }
};

使用此模式需满足三个条件:

  1. 目标类继承 std::enable_shared_from_this<T>
  2. 对象必须shared_ptr 管理(通过工厂方法强制约束)。
  3. 回调中捕获 weak_from_this(),而非 thisshared_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_ptrweak_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:优先使用工厂函数