同步与线程注解 · Mutex、SequenceChecker、RTC_GUARDED_BY
读 WebRTC 源码满眼 RTC_GUARDED_BY、RTC_RUN_ON——这些不是注释,是编译器能检查的线程安全注解。这一章讲 Mutex/Event/SequenceChecker 这些同步原语,以及 WebRTC 那套「用线程归属代替锁、让编译器帮你抓竞争」的独特哲学。
同步与线程注解 · Mutex、SequenceChecker、RTC_GUARDED_BY
一路读源码,你一定见过密密麻麻的 RTC_GUARDED_BY(worker_thread())、RTC_RUN_ON(network_thread())。它们看着像注释,其实是编译器能检查的线程安全约束。这一章讲 WebRTC 的同步原语,以及它那套颇有特色的并发纪律。
先给同步原语
rtc_base 提供跨平台的同步原语(呼应它吃掉平台差异的使命):
| 原语 | 源码 | 作用 |
|---|---|---|
Mutex | synchronization/mutex.h | 互斥锁(平台后端:abseil / pthread / Win 临界区) |
Event | event.h | 事件:一个线程等,另一个线程 set 唤醒 |
SequenceChecker | synchronization/sequence_checker_internal.h | 运行期检查“这段代码是否在预期线程 / 序列上跑” |
Mutex 按平台有不同后端(mutex_abseil.h / mutex_pthread.h / mutex_critical_section.h),上层只用 Mutex 一个名字——又一次平台差异被封在底下。但真正体现 WebRTC 特色的,不是锁本身,而是它尽量不用锁。
核心哲学:用线程归属代替锁
WebRTC 的并发哲学是“能不加锁就不加锁”——不是靠 Mutex 保护共享数据,而是靠“规定每份数据只属于某一条线程”,跨线程访问一律通过 PostTask 投递到那条线程,从而根本不产生竞争。 回想上一章说的:Thread 是任务队列、任务串行执行。于是:
- 一份数据“归属”某线程 —— 比如
Call的媒体状态归 worker 线程、网络状态归 network 线程。 - 只在归属线程访问它 —— 因为该线程的任务串行跑,同一时刻只有一个任务在碰这份数据,没有并发,自然不需要锁。
- 别的线程想碰?投递过去 ——
PostTask到归属线程,让访问在正确的线程上排队进行。
这比“共享 + 加锁”更快也更不易错:没有锁竞争、没有死锁、没有忘记加锁的 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 部分线程模型。