深入理解 WebRTC 临界锁实现与 C++ RAII 机制

深入理解 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 库
  • 支持线程安全注解

新实现的优势

  1. 更严格的正确性检查:非递归锁能尽早发现设计问题
  2. 更好的性能:非递归锁开销更小
  3. 统一的实现:基于 Abseil,与 Chromium 生态一致
  4. 线程安全注解:编译期检查锁的使用是否正确

【扩展】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 而非手动加解锁
优先非递归锁 尽早暴露设计问题
控制锁粒度 只保护必要的代码,减少持锁时间
避免嵌套锁 如必须嵌套,确保顺序一致
使用注解 利用编译器检查锁的正确使用

扩展阅读

相关推荐
i_am_a_div_日积月累_2 小时前
el-table实现自动滚动;列表自动滚动
开发语言·javascript·vue.js
weixin_307779132 小时前
Jenkins Jackson 2 API插件详解:JSON处理的基础支柱
运维·开发语言·架构·json·jenkins
JANGHIGH2 小时前
c++ 多线程(一)
开发语言·c++
匠心网络科技2 小时前
前端学习手册-JavaScript条件判断语句全解析(十八)
开发语言·前端·javascript·学习·ecmascript
神仙别闹2 小时前
基于C++生成树思想的迷宫生成算法
开发语言·c++·算法
海上彼尚2 小时前
Go之路 - 1.gomod指令
开发语言·后端·golang
我命由我123452 小时前
Java 开发使用 MyBatis PostgreSQL 问题:使用了特殊字符而没有正确转义
java·开发语言·数据库·postgresql·java-ee·mybatis·学习方法
C语言小火车2 小时前
红黑树(C/C++ 实现版)—— 用 “带配重的书架” 讲透本质
c语言·开发语言·c++·红黑树
阿里嘎多学长2 小时前
2025-12-10 GitHub 热点项目精选
开发语言·程序员·github·代码托管