# 三大线程模型 · network / worker / signaling

> WebRTC 是重度多线程的，而它的世界几乎完全由三条线程组织：signaling（控制）、worker（媒体处理）、network（收发包）。搞懂谁在哪条线程上跑，是读懂任何模块的前提。这一章讲清三线程的分工、TaskQueue 抽象，以及源码里那些 RTC_DCHECK_RUN_ON 注解。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/webrtc/threading-model
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 三大线程模型 · network / worker / signaling

WebRTC 是**重度多线程**的系统——它必须是。想象一下：编码一帧 4K 图像要几毫秒，如果这几毫秒里网络包收不进来、API 调用也卡住，通话就会抖。所以 WebRTC 把工作**切到不同线程并行跑**。而它的整个世界，几乎完全由**三条线程**组织。不理解它们，你读任何模块都会一头雾水。

## 三条线程

`PeerConnection` 通过 `ConnectionContext` 持有三条线程，`pc/connection_context.h` 里看得清清楚楚：

```cpp
// pc/connection_context.h(节选)
Thread* signaling_thread();
Thread* worker_thread();
Thread* network_thread();
```

它们分工明确：

| 线程 | 负责 | 特点 |
| --- | --- | --- |
| **signaling（信令）** | `PeerConnection` 的**所有 API 调用**、SDP 协商、状态机、观察者回调 | 控制面，逻辑复杂但不吃 CPU |
| **worker（工作）** | **媒体处理**：编解码调度、`Call` 与各 `modules` 的运转、码率分配 | 计算面，CPU 密集 |
| **network（网络）** | **收发包**：套接字 I/O、ICE 传输、包的路由分发 | I/O 面，延迟最敏感 |

一句话记住：**signaling 管「控制」，worker 管「算」，network 管「收发」。**

```mermaid
flowchart LR
    APP["应用代码"] -->|"createOffer / addTrack…"| SIG
    subgraph WebRTC
        SIG["signaling 线程<br/>PeerConnection·SDP·状态机"]
        WRK["worker 线程<br/>Call·编解码·码率分配"]
        NET["network 线程<br/>套接字·ICE·收发包"]
        SIG <-->|"派发任务"| WRK
        WRK <-->|"派发任务"| NET
    end
    NET <-->|"UDP 包"| PEER["对端 / 网络"]
    SIG -->|"回调(OnIceCandidate 等)"| APP
```

## 为什么偏偏是「线程」这么重要

因为在 WebRTC 里，**「一个对象属于哪条线程」是它的硬约束**——很多对象**只能在自己的归属线程上被创建、调用、销毁**，在别的线程上碰它就是 bug。`g3doc/threading_design.md` 讲得很直白：公共 API 被设计为**从单一「客户端线程」调用**，库内部再**自行派发**到该干活的线程去。

这带来一个反直觉的后果，文档特意警告：

> writing "increment(x); increment(x)" is not a safe way to increment X by exactly two

因为哪怕看起来最简单的调用，内部也可能**跳到别的线程**执行，两次调用之间，别的线程可能已经插进来干了一堆事。**在 WebRTC 里，「顺序执行」不是理所当然的**——这正是多线程模型必须刻在脑子里的原因。

## 线程注解：代码自己声明「我该在哪跑」

WebRTC 不靠文档口头约定线程归属，而是**把它写进代码、让编译器和运行期一起检查**。你会在源码里**铺天盖地**看到两个宏：

<Callout type="info">
  **`RTC_DCHECK_RUN_ON` 与 `RTC_GUARDED_BY` 是读 WebRTC 的路标——它们把「线程归属」变成可检查的断言。**

  - **`RTC_DCHECK_RUN_ON(线程)`**：放在函数体开头，断言「**此函数必须在这条线程上被调用**」。跑错线程，debug 版直接 crash，一眼抓出违规。
  - **`RTC_GUARDED_BY(线程)`**：放在成员变量声明后，声明「**这个变量只能在这条线程上访问**」，由静态分析在编译期检查。

  `pc/peer_connection.h` 里俯拾皆是，例如：

  ```cpp
  // pc/peer_connection.h(节选)
  PeerConnectionObserver* observer_ RTC_GUARDED_BY(signaling_thread()) = ...;
  bool was_ever_connected_ RTC_GUARDED_BY(signaling_thread()) = false;
  // 函数体里:
  RTC_DCHECK_RUN_ON(signaling_thread());
  ```

  **读代码时先看这些注解**——它立刻告诉你「这段逻辑活在哪条线程」，是理解一个模块最快的入口。看到 `RTC_DCHECK_RUN_ON(network_thread())`，你就知道这是收发包的热路径、得快；看到 `RTC_GUARDED_BY(signaling_thread())`，你就知道这个状态属于控制面。
</Callout>

## 「线程」其实是「顺序任务队列」

这里要纠正一个直觉。WebRTC 说的「thread」，**本质不是操作系统线程，而是「保证顺序执行的任务队列」**。`g3doc/threading_design.md` 的脚注点明：

> The term "thread" is used here to denote any construct that guarantees sequential execution - other names for such constructs are task runners and **sequenced task queues**.

这个抽象叫 **`TaskQueue`**（`rtc_base/`，[第 10 部分](https://daiw.org/manual/webrtc/reading-the-source)细讲）。它的关键性质是：**投进同一个队列的任务，一个接一个顺序执行，绝不并发**。于是「在某条线程上」= 「在某个 `TaskQueue` 里排队」，而**线程归属带来的安全性**——同一队列内不会并发访问——就有了保证。一条 OS 线程通常承载一个 `TaskQueue`，但概念上二者是解耦的。

跨线程干活，靠的就是**往目标队列投任务**。比如 signaling 线程上的逻辑需要动媒体，就把一段活儿 post 到 worker 线程：

```cpp
// 伪代码:把工作派发到 worker 线程的队列,顺序执行
worker_thread()->PostTask([this] {
  RTC_DCHECK_RUN_ON(worker_thread());   // 到了对的线程
  // …在 worker 线程上安全地干活…
});
```

投递可以是**异步**（`PostTask`，不等结果）或**阻塞**（等它跑完再返回）。后者正是[下一章「代理模式」](https://daiw.org/manual/webrtc/proxy-pattern)的核心机制：代理帮你把跨线程调用**阻塞地**跳过去，让你在**任意**线程调 API 都安全。

## 回调在哪条线程

反过来，库通知应用（ICE 候选、连接状态变化、远端轨道到达）走 `PeerConnectionObserver`。`g3doc/index.md` 说明：这些回调**多数在 signaling 线程**上触发（准确说，是创建工厂时指定的那条 signaling 线程）。所以 `g3doc/threading_design.md` 给应用一条铁律：**别在回调里直接回调 WebRTC 的函数**——而应把活儿派发到你自己的线程再做，否则容易踩线程 / 重入的坑。

## 小结与下一章

WebRTC 的世界由**三条线程**组织：**signaling**（控制：API、SDP、状态机、回调）、**worker**（计算：`Call`、编解码、码率分配）、**network**（收发：套接字、ICE、包路由）。「线程」本质是**顺序任务队列 `TaskQueue`**，跨线程靠**投任务**。源码用 `RTC_DCHECK_RUN_ON` / `RTC_GUARDED_BY` 把线程归属**写死进代码**——它们是你读任何模块最快的路标。

三线程模型带来一个问题：应用可能从任意线程调 API，可组件却只能在归属线程上被碰——**这个矛盾谁来抹平？** 答案是 WebRTC 一个精巧的设计：**代理对象**。下一章拆开它。👉 [代理模式与线程安全](https://daiw.org/manual/webrtc/proxy-pattern)。

## 参考资料

- [1] libwebrtc 源码.`g3doc/threading_design.md`（单客户端线程、顺序执行、TaskQueue）。
- [2] libwebrtc 源码.`pc/connection_context.h`（三线程持有）、`pc/peer_connection.h`（`RTC_DCHECK_RUN_ON` / `RTC_GUARDED_BY` 用例）。
- [3] libwebrtc 源码.`rtc_base/thread.h` / `TaskQueue`（线程与任务队列实现，第 10 部分）。
