第二章 · 核心架构与线程模型 本章要回答的三个问题:一次 调用从接口层一路走到编码器,中间穿过了哪些层、每层的职责边界在哪里?引擎内部为什么坚持用固定线程加任务队列的并发纪律,而不是常见的加锁方案?看源码时怎么判断一段代码被允许跑在哪个线程上,跑错了会发生什么? 为什么会有这一章 电话总局里有句老话:不认识科室分布,就别碰配线架。libwebrtc 上百万行代码,如果没有一张分层地图,读任何一个函数都会陷进去——它调了下游,又被上游调,引用横跨七八个目录。这一章就是那张地图,外加这座总局最特别的一条局规:线程纪律。 先说分层。
本章要回答的三个问题:一次
addTrack调用从接口层一路走到编码器,中间穿过了哪些层、每层的职责边界在哪里?引擎内部为什么坚持用固定线程加任务队列的并发纪律,而不是常见的加锁方案?看源码时怎么判断一段代码被允许跑在哪个线程上,跑错了会发生什么?
电话总局里有句老话:不认识科室分布,就别碰配线架。libwebrtc 上百万行代码,如果没有一张分层地图,读任何一个函数都会陷进去——它调了下游,又被上游调,引用横跨七八个目录。这一章就是那张地图,外加这座总局最特别的一条局规:线程纪律。
先说分层。引擎的目录结构本身就是分层的具象:接口层对外稳定,连接层管协商与生命周期,呼叫层是媒体中枢,引擎适配层把音视频共性收拢,模块层装着编解码与传输的核心算法,底座层提供线程、任务队列、网络套接字这些公共设施。依赖方向严格自上而下,上层可以调下层,下层绝不知道上层的存在。这个纪律换来的是模块可替换——换个编码器、换个传输实现,只要接口不变,上层无感。
再说线程。引擎内部的并发方案与多数业务代码的直觉相反:它几乎不用大锁,而是把"数据只能被特定线程触碰"写成铁律,跨线程通信一律通过投递任务完成。消息从网络线程进来,打包投递给工作线程解码,再投递给音频线程播放,全程没有一块共享缓冲区被两个线程同时抓着。这套纪律让数据竞争在架构层面消失,代价是任何嵌入者都必须搞清自己的代码跑在哪个线程上——这也正是"Called on wrong thread"这类断言崩溃成为新手第一坑的原因。

对照三个问题给结果。其一,你能在脑中画出一次媒体发送的完整穿越路径:接口层的轨道句柄、连接层的会话管理、呼叫层的发送流、引擎适配层的编码封装、模块层的打包与平滑发送,每一层叫什么、为什么必须存在。其二,你能解释任务队列方案相对大锁方案的优势:把"共享"变成"移交",锁竞争变成队列排队,代价可控且死锁面大幅缩小;同时你也会知道它的成本——跨线程操作都有排队延迟,实时路径上为什么宁可复制也不传递复杂对象。其三,你拿到判断线程归属的三个工具:接口文档的线程约定、断言宏的运行期检查、线程标签在日志中的呈现,嵌入自己的采集或渲染代码时能主动把调用投递到正确线程,而不是等断言崩了再查。
| 节号 | 回答哪个问题 | 关键产出 |
|---|---|---|
| 2.1 | 一条媒体数据怎么穿过整套系统 | 发送侧调用链全景 |
| 2.2 | 并发为什么这样设计,怎么不踩雷 | 线程归属表与投递范式 |
两节是"静"与"动"的关系:2.1 看静态的层与调用方向,2.2 看动态的执行载体。顺序建议不要颠倒——很多线程纪律只有落到具体调用链上才显得必要,先看链再看规,接受度高得多。
需要第一章的工程环境(能编译、能看日志),以及 C++ 多线程的基本概念:知道线程、互斥锁、条件变量是什么,不必有 lock-free 经验。2.1 的调用链分析会频繁出现接口与实现分离的写法,如果你对"接口指针背后是谁"还不熟,先把第一章术语表里的接口命名规律复习一遍。
本章出口指向第三章的连接建立:状态机与协商逻辑全部活在连接层,且严格约束在信令线程上执行——没有本章的线程概念,第三章那些回调时序根本无从谈起。媒体相关的第四、五、六章则默认你已理解 2.1 的调用链骨架,因为那三章实际上就是把这条链的各段拆开精读。
阅读方法上给一条建议:两节都配了可动手的实验,读到哪验证到哪。2.1 的调用链可以在演示程序上打断点逐站走一遍,记录每站的线程名;2.2 的线程纪律可以故意写一段错误线程的调用,亲眼看断言把问题钉在调用栈上。机制类知识亲手触发一次现象,胜过反复阅读十遍。