深入理解 WebRTC 临界锁实现与 C++ RAII 机制
本文基于 WebRTC M85 (branch-heads/4183) 版本源码分析,同时涉及 M86 版本的演进。
1. 引言
一段"奇怪"的代码
在阅读 WebRTC 源码时,你一定会频繁看到这样的代码:
cpp
void RtpVideoSender::SetFecAllowed(bool fec_allowed) {
rtc::CritScope cs(&crit_); // crit_ 的类型是 rtc::CriticalSection
fec_allowed_ = fec_allowed;
}
初学者往往会感到困惑:创建了一个局部变量 cs,却从未使用它,这有什么意义?
如果你有 Java 或 Python 背景,可能会期望看到类似 cs.lock() 和 cs.unlock() 的显式调用。但在 C++ 中,这段代码已经完成了加锁和解锁的全部工作。
秘密就在于 C++ 的构造函数 和析构函数会被自动调用:
- 当
cs被创建时,构造函数自动执行 → 加锁 - 当函数退出、
cs离开作用域时,析构函数自动执行 → 解锁
这就是 C++ 中著名的 RAII(Resource Acquisition Is Initialization) 机制。
2. CritScope 实现原理
源码解析
rtc::CritScope 的实现极其简洁,只有构造函数和析构函数:
cpp
class CritScope {
public:
explicit CritScope(const CriticalSection* cs) : cs_(cs) {
cs_->Enter(); // 加锁
}
~CritScope() {
cs_->Leave(); // 解锁
}
private:
const CriticalSection* const cs_;
// 禁止拷贝和赋值
RTC_DISALLOW_COPY_AND_ASSIGN(CritScope);
};
CriticalSection 的跨平台实现
CriticalSection 在不同平台上有不同的底层实现:
POSIX 平台(Linux / macOS / Android / iOS)
cpp
class CriticalSection {
public:
CriticalSection() {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 递归锁
pthread_mutex_init(&mutex_, &attr);
pthread_mutexattr_destroy(&attr);
}
~CriticalSection() {
pthread_mutex_destroy(&mutex_);
}
void Enter() const {
pthread_mutex_lock(&mutex_);
}
void Leave() const {
pthread_mutex_unlock(&mutex_);
}
private:
mutable pthread_mutex_t mutex_;
};
Windows 平台
cpp
class CriticalSection {
public:
CriticalSection() {
InitializeCriticalSection(&critical_section_);
}
~CriticalSection() {
DeleteCriticalSection(&critical_section_);
}
void Enter() const {
EnterCriticalSection(&critical_section_);
}
void Leave() const {
LeaveCriticalSection(&critical_section_);
}
private:
mutable CRITICAL_SECTION critical_section_;
};
【扩展】mutable 关键字的作用
注意到 mutex_ 被声明为 mutable。这是因为 Enter() 和 Leave() 被声明为 const 成员函数,但它们需要修改互斥锁的状态。mutable 允许在 const 成员函数中修改该成员变量。
这种设计使得 CriticalSection 可以作为 const 对象的成员,同时仍能正常工作。
3. RAII 机制详解
什么是 RAII
RAII(Resource Acquisition Is Initialization),直译为"资源获取即初始化",是 C++ 中管理资源的核心惯用法。
核心思想:
- 资源的获取 发生在对象的构造函数中
- 资源的释放 发生在对象的析构函数中
- 利用 C++ 的作用域规则,确保资源被正确释放
为什么 RAII 能保证异常安全
考虑以下场景:
cpp
void processData() {
rtc::CritScope cs(&crit_);
doSomething(); // 可能抛出异常
doSomethingElse(); // 可能抛出异常
} // 无论是否发生异常,cs 的析构函数都会被调用,锁一定会被释放
C++ 保证:当栈展开(Stack Unwinding)发生时,所有已构造的局部对象都会被正确析构 。这意味着即使 doSomething() 抛出异常,锁也会被正确释放,不会造成死锁。
与其他语言的对比
Java:try-finally
java
class DataProcessor {
private final ReentrantLock lock = new ReentrantLock();
public void processData() {
lock.lock();
try {
doSomething();
doSomethingElse();
} finally {
lock.unlock(); // 必须手动确保释放
}
}
}
问题:
- 代码冗长,每次加锁都需要 try-finally
- 容易遗漏 finally 块
- 嵌套锁时代码更加复杂
Golang:defer
go
func processData() {
mu.Lock()
defer mu.Unlock() // 延迟到函数返回时执行
doSomething()
doSomethingElse()
}
特点:
- 比 Java 简洁
- 但 defer 是函数级别的,无法精确控制作用域
- 多个 defer 按 LIFO 顺序执行,可能造成困惑
Python:with 语句
python
import threading
lock = threading.Lock()
def process_data():
with lock: # __enter__ 加锁,__exit__ 解锁
do_something()
do_something_else()
特点:
- 语法简洁
- 依赖上下文管理器协议(
__enter__/__exit__) - 与 RAII 思想相似,但不是语言内置机制
RAII 的优势总结
| 特性 | RAII (C++) | try-finally (Java) | defer (Go) | with (Python) |
|---|---|---|---|---|
| 代码简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 作用域精确控制 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| 不易遗漏 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 异常安全 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
【扩展】RAII 的其他应用场景
RAII 不仅用于锁,还广泛应用于:
cpp
// 智能指针
std::unique_ptr<MyClass> ptr(new MyClass()); // 自动释放内存
// 文件句柄
std::ifstream file("data.txt"); // 自动关闭文件
// 数据库连接
DatabaseConnection conn(config); // 自动断开连接
// 计时器
ScopedTimer timer("operation"); // 自动记录耗时
4. 递归锁 vs 非递归锁
什么是递归锁
递归锁(Recursive Lock) ,也称为可重入锁(Reentrant Lock),允许同一线程多次获取同一把锁而不会死锁:
cpp
void outer() {
rtc::CritScope cs(&crit_); // 第一次加锁
inner();
}
void inner() {
rtc::CritScope cs(&crit_); // 同一线程再次加锁,递归锁允许这样做
// ...
}
WebRTC M85 的 CriticalSection 使用的就是递归锁(PTHREAD_MUTEX_RECURSIVE)。
递归锁的缺点
虽然递归锁看起来很方便,但它存在严重的问题:
1. 隐藏设计问题
递归锁往往掩盖了代码设计上的缺陷。如果你需要递归锁,通常意味着:
- 函数职责不清晰
- 调用关系过于复杂
- 锁的粒度不合理
2. 性能开销
递归锁需要维护额外的状态:
- 记录当前持有锁的线程 ID
- 记录重入次数
- 每次加锁/解锁都需要额外检查
3. 难以调试
递归锁可能导致难以发现的问题:
cpp
void update() {
rtc::CritScope cs(&crit_);
data_ = computeNewValue(); // 如果 computeNewValue() 也加锁,递归锁会"成功"
notify(); // 但这可能不是你期望的行为
}
使用非递归锁时,上述代码会立即死锁,问题暴露得更早。
WebRTC M86 的重构
从 M86 版本开始,WebRTC 废弃了 rtc::CritScope,改用 webrtc::Mutex:
cpp
// 新的实现
class Mutex {
public:
void Lock() { impl_.Lock(); }
void Unlock() { impl_.Unlock(); }
private:
absl::Mutex impl_; // 非递归锁
};
class MutexLock {
public:
explicit MutexLock(Mutex* mutex) : mutex_(mutex) {
mutex_->Lock();
}
~MutexLock() {
mutex_->Unlock();
}
private:
Mutex* const mutex_;
};
这次重构的背景是 Issue 11567,主要目标是:
- 使用非递归锁,尽早暴露设计问题
- 统一使用 Abseil 库的实现
- 增强线程安全检查
【扩展】如何选择锁类型
| 场景 | 推荐 |
|---|---|
| 新代码 | 非递归锁 |
| 函数可能被递归调用 | 重构代码,避免递归加锁 |
| 遗留代码迁移 | 先用递归锁,逐步重构 |
| 性能敏感场景 | 非递归锁(开销更小) |
5. WebRTC 锁机制的演进
M85 及之前
cpp
// 声明
rtc::CriticalSection crit_;
// 使用
void SomeMethod() {
rtc::CritScope cs(&crit_);
// critical section...
}
特点:
- 递归锁
- 跨平台封装(pthread / Windows API)
- 简单易用
M86 及之后
cpp
// 声明
webrtc::Mutex mutex_;
// 使用
void SomeMethod() {
webrtc::MutexLock lock(&mutex_);
// critical section...
}
特点:
- 非递归锁
- 基于 Abseil 库
- 支持线程安全注解
新实现的优势
- 更严格的正确性检查:非递归锁能尽早发现设计问题
- 更好的性能:非递归锁开销更小
- 统一的实现:基于 Abseil,与 Chromium 生态一致
- 线程安全注解:编译期检查锁的使用是否正确
【扩展】Abseil 库在 WebRTC 中的应用
WebRTC 大量使用 Google 的 Abseil 库:
cpp
#include "absl/synchronization/mutex.h"
absl::Mutex mutex_;
absl::MutexLock lock(&mutex_);
// 还支持读写锁
absl::ReaderMutexLock read_lock(&mutex_);
absl::WriterMutexLock write_lock(&mutex_);
6. 最佳实践与总结
何时使用 RAII 风格的锁
始终使用 RAII 风格 ,避免手动调用 Lock() / Unlock():
cpp
// ✅ 推荐
void good() {
MutexLock lock(&mutex_);
// ...
}
// ❌ 避免
void bad() {
mutex_.Lock();
// ... 如果这里抛出异常,锁不会被释放
mutex_.Unlock();
}
锁的粒度控制
cpp
// ✅ 只保护必要的代码
void good() {
Data data;
{
MutexLock lock(&mutex_);
data = shared_data_; // 只在临界区内访问共享数据
}
processData(data); // 耗时操作在锁外执行
}
// ❌ 持锁时间过长
void bad() {
MutexLock lock(&mutex_);
Data data = shared_data_;
processData(data); // 耗时操作也在锁内,阻塞其他线程
}
避免在持锁期间调用可能阻塞的函数
cpp
// ❌ 危险:可能导致死锁
void dangerous() {
MutexLock lock(&mutex_);
callback_(); // 回调函数可能尝试获取同一把锁
}
// ✅ 安全:先复制,再调用
void safe() {
Callback callback;
{
MutexLock lock(&mutex_);
callback = callback_;
}
callback(); // 在锁外调用
}
使用线程安全注解
WebRTC 使用 Clang 的线程安全注解进行编译期检查:
cpp
class MyClass {
public:
void SetValue(int value) RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
value_ = value;
}
int GetValue() const RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
return value_;
}
private:
Mutex mutex_;
int value_ RTC_GUARDED_BY(mutex_);
};
总结
| 要点 | 说明 |
|---|---|
| 使用 RAII | 始终使用 MutexLock 而非手动加解锁 |
| 优先非递归锁 | 尽早暴露设计问题 |
| 控制锁粒度 | 只保护必要的代码,减少持锁时间 |
| 避免嵌套锁 | 如必须嵌套,确保顺序一致 |
| 使用注解 | 利用编译器检查锁的正确使用 |