引用计数 · scoped_refptr 与对象的生死

WebRTC 的对象在多线程间传来传去,谁负责销毁?答案是引用计数。这一章读 scoped_refptr.h / ref_counted_object.h,讲清 RefCountInterface、RefCountedObject、make_ref_counted、scoped_refptr 这套机制,以及它为什么是异步多线程代码的生命线。

作者 David更新于 第 94 篇(共 112 篇)

引用计数 · scoped_refptr 与对象的生死

上一章说,WebRTC 靠 PostTask 在线程间投递任务。但这带来一个尖锐问题:一个对象被投递到别的线程异步使用,执行时它还活着吗?会不会已经被原线程销毁了?WebRTC 的答案是引用计数——这一章看它。

问题:异步世界里,谁来销毁对象

同步单线程代码里,对象生死很清楚:出了作用域就析构。但 WebRTC 是异步多线程的:

在异步代码里,一个对象可能被多个线程、多个回调同时引用,“谁最后用完谁负责销毁”用手动 delete 根本管不清——所以 WebRTC 用引用计数**:对象记录“有多少人正引用我”,降到 0 才自我销毁。** 想象 PeerConnection 被应用持有、又被内部若干组件引用、还被投递到别的线程的任务捕获——它到底何时能销毁?没有一个中心知道全部引用。引用计数把这个判断分散化:每个持有者“登记”(计数 +1)、“注销”(计数 -1),对象自己在计数归 0 时析构。没有谁需要知道全局,对象自己管自己的生死。 这是异步多线程代码管理生命周期的经典解法(也是 std::shared_ptr 的思路)。

三个零件:接口、实现、智能指针

WebRTC 的引用计数由三部分组成(比 shared_ptr 更“侵入式”——计数长在对象自己身上):

1. RefCountInterface:计数接口

// api/ref_count.h(节选)
class RefCountInterface {
 public:
  virtual void AddRef() const = 0;
  virtual RefCountReleaseStatus Release() const = 0;   // 返回是否"刚丢掉最后一个引用"
};

任何想被引用计数管理的类,实现这个接口:AddRef 计数 +1,Release 计数 -1 并在归 0 时返回 kDroppedLastRef。

2. RefCountedObject<T>:自动加计数的实现

你不用手写计数逻辑,RefCountedObject<T> 帮你把原子计数混入任意类 T(真实源码 ref_counted_object.h:21):

// ref_counted_object.h(节选)
template <class T>
class RefCountedObject : public T {           // 继承 T,给它加上引用计数
  RefCountReleaseStatus Release() const override {
    // 原子减,归 0 时 delete this
    if (status == RefCountReleaseStatus::kDroppedLastRef) { delete this; }
    ...
  }
  mutable webrtc::webrtc_impl::RefCounter ref_count_{0};   // 原子计数器
};

通常用工厂函数 make_ref_counted<MyClass>(args...) 创建——它 new 一个 RefCountedObject<MyClass> 并返回 scoped_refptr。原子计数保证多线程 AddRef/Release 安全。

3. scoped_refptr<T>:RAII 智能指针

手动 AddRef/Release 容易忘、容易漏——所以几乎没人手动调。你用 scoped_refptr<T>,它在构造 / 拷贝时自动 AddRef、析构时自动 Release(真实源码 scoped_refptr.h):

// scoped_refptr.h(节选,精简)
template <typename T>
class scoped_refptr {
  scoped_refptr(const scoped_refptr& r) : ptr_(r.get()) { if (ptr_) ptr_->AddRef(); }
  ~scoped_refptr() { if (ptr_) ptr_->Release(); }      // 出作用域自动 -1
  scoped_refptr(scoped_refptr&& r) noexcept : ptr_(r.release()) {}  // 移动不加计数
  T* get() const { return ptr_; }
 private:
  T* ptr_;
};

scoped_refptr<T> 是 WebRTC 版的 shared_ptr——拷贝就 +1、析构就 -1、移动零成本,你只管传递它,计数全自动。 关键行为:

  • 拷贝 = 多一个引用 —— scoped_refptr 传参 / 赋值,计数 +1,对象因此“多活一会儿”。
  • 析构 = 少一个引用 —— 出作用域自动 -1,最后一个走的负责销毁。
  • 移动 = 转移所有权 —— std::move 一个 scoped_refptr 不改计数(release() 交出裸指针),零开销。

于是投递到别的线程的任务只要捕获一个 scoped_refptr,就等于给对象续了命——任务没跑完,计数就不为 0,对象就不会被销毁。引用计数 + scoped_refptr 是 WebRTC 异步安全的生命线。

为什么到处是 scoped_refptr

现在回头看前面各部分,就懂了:PeerConnectionInterface、MediaStreamTrackInterface、RtpSenderInterface、RTCCertificate……WebRTC 的 API 对象几乎清一色用 scoped_refptr 返回和传递。原因就是上面说的:这些对象跨线程、被多方持有、生命周期不确定,必须用引用计数管理。CreatePeerConnection 返回 scoped_refptr<PeerConnectionInterface>——你现在知道这个类型不是随便选的,而是异步架构的必然。

图表加载中…

小结与下一章

WebRTC 用引用计数管理异步多线程下的对象生死:对象记“多少人引用我”、归 0 自毁,免去了“谁负责 delete”的难题。三个零件:RefCountInterface(AddRef/Release 接口)、RefCountedObject<T>(把原子计数混入任意类,归 0 delete this,配 make_ref_counted)、scoped_refptr<T>(RAII 智能指针:拷贝 +1、析构 -1、移动零成本)。跨线程任务捕获 scoped_refptr 即给对象续命——这是 WebRTC 异步安全的生命线,也是API 对象清一色 scoped_refptr 的原因。

对象在系统里传递时,还有一样东西天天在传——媒体数据缓冲。它多到必须“零拷贝”。下一章看 WebRTC 的缓冲设计。👉 零拷贝缓冲。

参考资料

  • [1] libwebrtc 源码.api/scoped_refptr.h、api/ref_count.h、rtc_base/ref_counted_object.h、rtc_base/ref_counter.h。
  • [2] libwebrtc 源码.api/make_ref_counted.h(make_ref_counted 工厂)。
  • [3] 关联线程与任务队列、代理模式、工厂。

本页目录