临界锁实现
在构造函数中加锁,在析构函数中解锁 🧐
本文所有源码均基于 WebRTC M85 (branch-heads/4183) 版本进行分析。
在阅读 WebRTC 源码过程中,经常可以看到 rtc::CritScope
相关的代码调用,例如:
rtp_video_sender.cc
void RtpVideoSender::SetFecAllowed(bool fec_allowed) {
rtc::CritScope cs(&crit_); // rtc::CriticalSection crit_; fec_allowed_ = fec_allowed;
}
笔者目前的主力语言还不是 C++,所以第一次见到这种加锁机制还挺新鲜的。事实上笔者刚开始甚至以为这只是创建了一个 cs
变量,然后什么都不做,不知道这样的代码有什么意义。
但这其实是 C++ 编程的小技巧。我们先来看看 rtc::CritScope
的具体实现:
critical_section.cc
// CritScope 只有构造函数和析构函数两个定义;
// CriticalSection 在不同平台上的实现不一样,
// 对于 POSIX 而言实现为 mutable pthread_mutex_t mutex_;
CritScope::CritScope(const CriticalSection* cs) : cs_(cs) {
cs_->Enter(); // pthread_mutex_lock(&mutex_);
}
CritScope::~CritScope() {
cs_->Leave(); // pthread_mutex_unlock(&mutex_);
}
在 C++ 中,函数内部的局部变量会在该函数退出时进行析构(不论是否有异常)。通过在局部变量的构造函数中加锁,在析构函数中解锁,可以有效创造出一段函数生命周期内的临界区,而不用撰写类似 Java ReentrantLock 的 try-finally 释放锁的冗余代码:
ReentrantLock
class X {
private final ReentrantLock lock = new ReentrantLock();
// other definitions...
public void m() {
lock.lock(); // block until condition holds
try {
// method body...
} finally {
lock.unlock()
}
}
}
更进一步来说,这其实是 C++ RAII(资源获取即初始化,Resource Acquisition Is Initialization)机制的一种使用场景。RAII 可以保证在释放资源时不受到异常退出的影响(即使发生了异常,也能正确释放资源);同时还能预防编码过程忘记释放资源的行为。在笔者看来,RAII 是比 Golang 的 defer 机制更加简洁的存在,哈哈。
从 WebRTC M86 (branch-heads/4240) 版本开始 rtc::CritScope
被废弃,改为使用新的 webrtc::Mutex
实现。这是因为前者为递归锁(可重入),存在一些难以解决的问题 1;需要改为非递归锁(不可重入)。关于递归锁的缺点,亦可参见笔者的 这篇博客。