# 代理模式与线程安全 · Proxy 如何抹平线程

> 应用可能从任意线程调 API，组件却只能在归属线程上被碰——WebRTC 用「代理对象」抹平这个矛盾：每次调用都被自动、阻塞地转发到正确的线程执行。这一章拆开 pc/proxy.h 的宏魔法，讲清 Interface / Proxy / Impl 三层如何协作保证线程安全。

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

# 代理模式与线程安全 · Proxy 如何抹平线程

上一章留了个矛盾：应用可能从**任意线程**调 `PeerConnection` 的方法，可 `PeerConnection` 的实现**只能在 signaling 线程上被碰**。这两者怎么调和？WebRTC 的答案，是一个贯穿全库的精巧设计——**代理对象（Proxy）**。理解它，你就理解了 [Interface / Proxy / Impl 三副面孔](https://daiw.org/manual/webrtc/source-map)背后的运作。

## 问题：调用方在哪，实现要求在哪，对不上

把矛盾摆明：

- **实现对象 `PeerConnection`**：内部状态用 `RTC_GUARDED_BY(signaling_thread())` [保护](https://daiw.org/manual/webrtc/threading-model)，**只能在 signaling 线程上安全访问**。
- **应用**：可能在 UI 线程、网络回调线程、随便哪条线程上，想调 `pc->CreateOffer(...)`。

如果让应用的调用**直接**打到实现对象上，就会在**错误的线程**上访问受保护的状态——轻则数据竞争，重则崩溃。总不能要求每个应用开发者「调 WebRTC 前先手动切到 signaling 线程」吧？太反人性。

## 解法：在中间插一个代理

WebRTC 的做法是，在 `Interface` 和 `Impl` 之间**插一层 `Proxy`**：

<Callout type="info">
  **代理对象和实现对象实现同一个接口，但代理的每个方法只做一件事：把调用「阻塞地」转发到实现对象的归属线程，在那条线程上执行真正的实现，再把结果带回来。** `g3doc/threading_design.md` 的原话：

  > Many APIs are defined in terms of a "proxy object", which will do a **blocking dispatch** of the function to another thread, and an "implementation object" which will do the actual work, but can only be created, invoked and destroyed on its "home thread".

  于是**应用在任意线程调用都安全**：你调的其实是 `PeerConnectionProxy`，它替你**跳到 signaling 线程**，在那儿调真正的 `PeerConnection`，等它跑完、把返回值递给你。从调用方看，这就是一次普通的同步函数调用——线程的事，代理默默抹平了。
</Callout>

三层协作，一张图：

```mermaid
flowchart LR
    APP["应用<br/>(任意线程 T)"] -->|"pc->CreateOffer()"| PROXY["PeerConnectionProxy<br/>(线程 T 上被调)"]
    PROXY -->|"阻塞派发到 signaling 线程"| IMPL["PeerConnection<br/>(signaling 线程上执行)"]
    IMPL -->|"返回值"| PROXY
    PROXY -->|"返回给调用方"| APP
```

## 宏魔法：proxy.h 怎么少写几千行

几十个接口、每个接口几十个方法，如果每个代理方法都手写「切线程、调用、返回」的样板，得写到天荒地老。WebRTC 用**一组宏**自动生成这些代理，全在 `pc/proxy.h`。它文件头的示例用法，把机制讲得一清二楚：

```cpp
// pc/proxy.h 文件头注释里的示例
BEGIN_PROXY_MAP(Test)
  PROXY_PRIMARY_THREAD_DESTRUCTOR()
  PROXY_METHOD0(std::string, FooA)
  PROXY_CONSTMETHOD1(std::string, FooB, arg1)
  PROXY_SECONDARY_METHOD1(std::string, FooC, arg1)
END_PROXY_MAP()
```

读法：

- **`BEGIN_PROXY_MAP(Test)` / `END_PROXY_MAP()`** —— 为 `Test` 生成一个 `TestProxy` 类。
- **`PROXY_METHOD0(返回类型, 方法名)`** —— 生成一个无参方法，转发到**主线程（primary thread）**。数字是参数个数（`PROXY_METHOD1` 一个参数，以此类推）。
- **`PROXY_SECONDARY_METHOD1(...)`** —— 转发到**次线程（secondary thread）**。
- **`PROXY_PRIMARY_THREAD_DESTRUCTOR()`** —— 连**析构**都要在主线程上做（实现对象「只能在归属线程销毁」）。

## 主线程与次线程

代理支持**两条**目标线程，`pc/proxy.h` 文件头写明它们通常是谁：

> the **primary thread** will be the PeerConnection's **signaling thread**, and the **secondary thread** will be either the PeerConnection's **worker thread** or the PeerConnection's **network thread**.

也就是说：**大部分方法**用 `PROXY_METHOD` 转发到 signaling 线程（[控制面](https://daiw.org/manual/webrtc/threading-model)）；**少数需要在 worker/network 线程上做的方法**，用 `PROXY_SECONDARY_METHOD` 转发到次线程。代理的创建函数因此长这样（`pc/proxy.h` 文件头）：

```cpp
// 传入两条线程 + 真正的实现对象
TestProxy::Create(Thread* signaling_thread, Thread* worker_thread,
                  TestInterface*);
```

还有一个变体 `BEGIN_PRIMARY_PROXY_MAP`，它**只认主线程**，所有方法都往 signaling 线程转发——适合完全不碰 worker/network 的组件。

## 代价：阻塞，以及「简单调用会跳线程」

代理很优雅，但不是没有代价，读源码和调性能时要记住两点：

- **阻塞派发有开销**：每次跨线程调用，调用线程都要**等**目标线程把这活儿排到、跑完。若目标线程正忙（比如 signaling 线程在处理一个大 SDP），你的调用就得排队等着。所以热路径上要尽量避免频繁的跨线程阻塞调用。
- **「简单」调用其实会跳线程**：这正好解释了[上一章](https://daiw.org/manual/webrtc/threading-model)那条警告——`increment(x); increment(x)` 为什么不安全。因为每个 `increment` 都是一次代理调用，会跳到目标线程执行；两次之间，目标线程可能已经处理了一大堆别的排队任务。**代理让「跨线程」变得透明，但透明不等于免费，更不等于原子。**

## 小结与下一章

**代理对象**抹平了「应用在任意线程调用」与「实现只能在归属线程执行」的矛盾：`Interface`（契约）/ `Proxy`（阻塞转发到正确线程）/ `Impl`（在归属线程真正干活）三层协作。`pc/proxy.h` 用 `BEGIN_PROXY_MAP` / `PROXY_METHOD0` / `PROXY_SECONDARY_METHOD` 等宏**自动生成**代理，主线程通常是 signaling、次线程是 worker 或 network。代价是**阻塞派发的开销**，以及「透明的跨线程调用并不原子」。

至此，导论的**架构地基**——[是什么](https://daiw.org/manual/webrtc/what-is-webrtc)、[源码地图](https://daiw.org/manual/webrtc/source-map)、[通话流程](https://daiw.org/manual/webrtc/call-lifecycle)、[线程](https://daiw.org/manual/webrtc/threading-model)、代理——已经就位。最后一章更实用：**怎么真正上手读这套源码**——构建系统、目录约定、命名规律，以及怎么对照本教程按图索骥。👉 [怎么读这套源码](https://daiw.org/manual/webrtc/reading-the-source)。

## 参考资料

- [1] libwebrtc 源码.`pc/proxy.h`（代理宏 `BEGIN_PROXY_MAP` / `PROXY_METHOD` 及文件头示例）。
- [2] libwebrtc 源码.`g3doc/threading_design.md`（proxy object 与 blocking dispatch）。
- [3] 设计模式.Proxy（代理）—— GoF 经典模式在多线程边界上的一次工业应用。
