同步与线程注解 · Mutex、SequenceChecker、RTC_GUARDED_BY

读 WebRTC 源码满眼 RTC_GUARDED_BY、RTC_RUN_ON——这些不是注释,是编译器能检查的线程安全注解。这一章讲 Mutex/Event/SequenceChecker 这些同步原语,以及 WebRTC 那套「用线程归属代替锁、让编译器帮你抓竞争」的独特哲学。

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

同步与线程注解 · Mutex、SequenceChecker、RTC_GUARDED_BY

一路读源码,你一定见过密密麻麻的 RTC_GUARDED_BY(worker_thread())、RTC_RUN_ON(network_thread())。它们看着像注释,其实是编译器能检查的线程安全约束。这一章讲 WebRTC 的同步原语,以及它那套颇有特色的并发纪律。

先给同步原语

rtc_base 提供跨平台的同步原语(呼应它吃掉平台差异的使命):

原语源码作用
Mutexsynchronization/mutex.h互斥锁(平台后端:abseil / pthread / Win 临界区)
Eventevent.h事件:一个线程等,另一个线程 set 唤醒
SequenceCheckersynchronization/sequence_checker_internal.h运行期检查“这段代码是否在预期线程 / 序列上跑”

Mutex 按平台有不同后端(mutex_abseil.h / mutex_pthread.h / mutex_critical_section.h),上层只用 Mutex 一个名字——又一次平台差异被封在底下。但真正体现 WebRTC 特色的,不是锁本身,而是它尽量不用锁。

核心哲学:用线程归属代替锁

WebRTC 的并发哲学是“能不加锁就不加锁”——不是靠 Mutex 保护共享数据,而是靠“规定每份数据只属于某一条线程”,跨线程访问一律通过 PostTask 投递到那条线程,从而根本不产生竞争。 回想上一章说的:Thread 是任务队列、任务串行执行。于是:

这比“共享 + 加锁”更快也更不易错:没有锁竞争、没有死锁、没有忘记加锁的 bug。锁只在少数“数据确实需要被多线程直接共享”的地方才用。这就是为什么 WebRTC 是三线程 / 多任务队列的架构——架构本身就是它的并发安全策略。

RTC_GUARDED_BY / RTC_RUN_ON:让编译器执行纪律

问题来了:“每份数据只在归属线程访问”是纪律,人是会犯错的——一不小心在错误的线程碰了某个变量,怎么办?WebRTC 的答案是让编译器帮你查:

// 线程安全注解(thread_annotations.h),Clang 编译期分析
class Call {
  worker_thread_;
  std::map<...> receive_streams_ RTC_GUARDED_BY(worker_thread_);  // 声明:此数据归 worker

  void AddStream() RTC_RUN_ON(worker_thread_) {   // 声明:此函数只在 worker 跑
    receive_streams_[ssrc] = ...;                 // ✅ 合法,在归属线程
  }
};
// 若从别的线程访问 receive_streams_,或没标 RTC_RUN_ON 就碰它 → 编译告警/失败

RTC_GUARDED_BY(x)、RTC_RUN_ON(x)、RTC_LOCKABLE、RTC_EXCLUSIVE_LOCKS_REQUIRED(x) 是 Clang 线程安全分析的注解(thread_annotations.h)——它们在编译期静态检查“这份数据是否只在该线程 / 持有该锁时被访问”,违反就报错。 于是“数据归属线程”这条纪律不再靠自觉,而是编译器强制:

  • RTC_GUARDED_BY(thread_or_mutex) —— 声明某成员“必须在持有某锁 / 在某线程时”才能访问。
  • RTC_RUN_ON(thread) —— 声明某函数“只应在某线程运行”,函数体里访问该线程的 GUARDED_BY 数据才合法。
  • 编译期报错 —— 写错线程访问,Clang 直接编译不过,bug 在敲代码时就被抓住,而非线上偶现的诡异崩溃。

这就是为什么前面各部分的源码里到处是这些标注——它们是 WebRTC 把并发纪律固化进类型系统的手段。你读源码时,RTC_GUARDED_BY(x) 是极有价值的信息:它直接告诉你“这份数据归谁、该在哪访问”,是理解一段代码线程模型的最快线索。

SequenceChecker:运行期的补充检查

编译期注解很强,但有些情况静态分析覆盖不到(比如通过接口指针的间接调用)。SequenceChecker 补上运行期检查:在对象里放一个 SequenceChecker,方法入口 RTC_DCHECK_RUN_ON(&sequence_checker_),一旦在错误的线程 / 序列上被调用,Debug 构建直接断言崩溃,把问题暴露在测试阶段。编译期(注解)+ 运行期(SequenceChecker)双保险,共同守护“数据只在归属线程访问”这条铁律。

小结与下一章

rtc_base 提供跨平台同步原语 Mutex(按平台后端)、Event、SequenceChecker。但 WebRTC 的核心哲学是用线程归属代替锁:规定每份数据归属某线程、只在该线程访问、跨线程靠 PostTask,从而无并发、免锁——架构本身即并发安全策略。这条纪律靠 Clang 线程安全注解(RTC_GUARDED_BY/RTC_RUN_ON 等,thread_annotations.h)在编译期强制、靠 SequenceChecker 在运行期补检——双保险。读源码时,RTC_GUARDED_BY(x) 是判断“数据归谁”的最快线索。

rtc_base 还有一样贯穿拥塞控制与统计的小工具——滑动窗口测速率。下一章看它,然后收尾第 10 部分。👉 速率统计 RateStatistics。

参考资料

  • [1] libwebrtc 源码.rtc_base/synchronization/mutex.h、rtc_base/event.h、rtc_base/synchronization/sequence_checker_internal.h。
  • [2] libwebrtc 源码.rtc_base/thread_annotations.h(RTC_GUARDED_BY/RTC_RUN_ON);Clang Thread Safety Analysis 文档。
  • [3] 关联线程与任务队列、第 0 部分线程模型。

本页目录