8. Double Buffer
8. Double Buffer:在后台缓冲完成整批写入后一次交换,使读者只见完整状态,通过可复位因果实验和反例证据验收。
8. Double Buffer
学习目标
- 能解释读写双缓冲的交换机制
- 能说明撕裂的成因与解法
- 能选择交换方式与缓冲粒度
为什么"8. Double Buffer"从问题证据开始
游戏里有个经典撕裂(tearing)问题:如果显示器直接在正在绘制的画面上采样,就会看到画到一半的画面——上半帧是新内容,下半帧还是旧的,视觉上像被劈开。原因是"写入"和"读取"同时发生在同一块内存上。
本页把"无模式基线"定义为"单缓冲(写哪读哪)"的代码,然后引入双缓冲作为候选机制,验证它是否真的让"读者永远看到完整帧"。通过条件是写入过程中读者看到的始终是上一帧的完整内容——而不是读到写了一半的混合状态。
🔮 先预测:单缓冲下写到一半被读取,画面会怎样?
来源、版本与独立重写边界
本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。
本章机制与术语
理解 Double Buffer 需要四个核心概念:
- ↡:正在被写入的缓冲,读者看不到——写方的私有工作区。
- ↡:正在被读取显示的缓冲,内容总是完整的。
- ↡:把后台与显示缓冲的角色一次性互换——读者瞬间看到新帧。
- ↡:读写同一块缓冲导致的画面错位——双缓冲要消灭的故障。
四个概念共同约束"在后台缓冲完成整批写入后一次交换,使读者只见完整状态":任何结论都必须回到"读者是否始终看到完整帧、交换是否原子、粒度是否合理"三个可观察量。
Double Buffer — 写完一整帧,再让读者看见
第 1 / 5 步 · ① Buffer A 写入新帧(后台缓冲,读者看不到写到一半)
两块缓冲轮换角色:写方永远写无人看的那块,读者永远读无人写的那块。
💡 对着动画看:动画是双栏结构——左侧 Buffer A(后台缓冲)在写入,右侧 Buffer B(显示缓冲)被读取,中间 swap() 闸门。注意写入时那些半透明的格子(写到一半的内容)读者永远看不到。读"交换本身耗时"一节时,想想 swap 如果是"拷贝数据"而不是"交换指针",会出什么问题。
官方结构逐项深读
8. Double Buffer
Double Buffer 的主张:用两块缓冲轮换角色——写方永远写"读者没在看"的那块,读者永远读"没人正在写"的那块。写完一整帧后做一次交换,读者立刻看到完整新帧。读写被彻底隔离,撕裂不可能发生。它本质是一个"写后读"的并发问题,用空间换时间解决。
Intent
意图:把"写入"与"读取"解耦到不同内存,让读者永远看不到写了一半的状态。适用场景是"一个系统在写、另一个系统在读、且两者频率不同步"——渲染器读、物理/逻辑写。核心合同:交换必须是原子的(读者要么看到旧帧要么看到新帧,绝无中间态)。
Motivation
除了图形,另一个经典场景是音频:音频设备持续播放缓冲,游戏逻辑要往缓冲里写新音效。如果直接写正在播放的缓冲,会听到爆音(读到半写的样本)。双缓冲让逻辑写"未播放"的缓冲,播完交换。还有游戏世界状态:AI 系统在遍历实体列表做决策时,如果其他系统同时修改列表,遍历就踩空——世界双缓冲让"读世界"和"改世界"分开。
How computer graphics work (briefly)
图形流水线的双缓冲:显卡有显存中的两个帧缓冲,一个被扫描输出(display),一个被 GPU 渲染(render)。渲染完调用交换(通常是指针/句柄交换),扫描输出立刻切到新帧。垂直同步(vsync)把交换与显示器刷新对齐,避免交换发生在扫描途中(否则还是可能看到半新半旧——这就是"撕裂"的残余形态)。
Act 1, Scene 1
作者用一个舞台剧比喻:幕布是读者,舞台是写方。让观众直接看舞台,换幕时就会看到搬运工搬道具(撕裂)。双缓冲 = 在后台搭好下一幕,然后拉一下幕布(swap)——观众看到的永远是完整的"一幕"。这个比喻点出双缓冲的本质:读者与写方之间隔着一层"完整状态"的承诺。
Back to the graphics
回到图形:CPU/GPU 渲染完整一帧到后台缓冲,期间显示缓冲继续输出上一帧;渲染完毕交换,显示新帧。这解释了为什么 60fps 游戏"每帧"其实渲染的是"上一帧的延续"——后台缓冲给了渲染一整个帧周期的时间,不受扫描输出节奏的干扰。
The Pattern
模式结构:一个"缓冲"抽象(数组/列表/状态集合),内部持两份实例(current_ 与 next_);写方操作 next_;读者读 current_;写完调用 swap() 互换两者。注意:写方永远写 next,读者永远读 current——"当前/下一"是角色而非固定对象,交换只是换角色。
When to Use It
何时用双缓冲:有写方与读者两个系统,且读者需要一致快照;写方无法在读者的读取窗口内完成原子更新。图形(渲染 vs 扫描)、音频(逻辑 vs 播放)、AI(决策 vs 世界状态)都是典型。何时不用:只有一个系统访问、或写入本身就是原子的(单变量赋值)——双缓冲只是浪费内存。
Keep in Mind
两个主要代价:内存翻倍(两块缓冲)、延迟增加(读者看到的是上一帧——"现在"永远慢一帧)。后者在需要低延迟反馈的场景(音游判定、竞技对战)要格外注意。交换本身也要设计:交换指针是 O(1),交换内容是 O(N)——设计决策小节详述。
The swap itself takes time
交换的成本取决于实现:交换指针/句柄 O(1)(推荐——两块缓冲本身不动,只换"谁是 current"的标记);拷贝数据 O(N)(每次交换拷贝整块内存,会吃掉性能)。选择依据:缓冲是否可原地复用(指针交换)还是必须复制(如历史状态需要保留)。作者强调:能交换引用就别拷贝内容。
We have to have two buffers
双缓冲的内存代价:任何"需要一致快照的读写"都要两份——这是硬性成本。但好消息是缓冲往往复用(图形帧缓冲、音频样本缓冲),不是每帧新分配。设计时考虑:两块缓冲是固定分配(启动时一次建好)而非按需分配(每帧 new/free),避免 GC 压力。
Sample Code
示例代码展示一个简单双缓冲:Buffer 类持 current_ 与 next_ 两个数组;write(i, v) 写 next_[i];read(i) 读 current_[i];swap() 交换两个指针。音频系统的完整示例:逻辑每帧写音效进 next,swap 后播放线程读 current——播放永远拿到完整一帧。
Not just for graphics
双缓冲不止图形:人工智能(决策基于世界快照,避免读到决策中改变的世界)、粒子系统(遍历更新粒子时,其他系统读的是上一帧的粒子状态)、ECS 查询(组件迭代期间防并发修改)。凡是"遍历者需要一致视图"的场景,双缓冲都是一个通用解。
Artificial unintelligence
AI 案例:多个 AI 实体同时决策,如果它们共享同一个"当前世界"并互相修改,决策结果互相污染(A 的决策依赖被 B 改过的世界)。双缓冲世界:所有 AI 基于 current 世界决策,把决策写入 next 世界,全部决策完成后 swap——决策的输入一致性被保证。这是"AI 竞态"的标准解法。
Buffered slaps
一个有趣的例子:格斗游戏的"连击判定"——如果判定直接读写同一状态,一次攻击触发多次判定(重入)。双缓冲把"这帧的攻击记录"与"上帧的攻击结算"分开:攻击只影响 next 缓冲,结算只读 current——重入问题自然消失。这个例子说明双缓冲也用于打破重入。
Design Decisions
双缓冲有两个关键设计决策:怎么交换(指针 vs 内容拷贝)和 缓冲粒度(整个状态一份 vs 按字段/按实体分块)。前者决定交换成本,后者决定内存开销与局部性——下面两节展开。
How are the buffers swapped?
交换方式三选一:指针交换(O(1),推荐——两块缓冲固定,仅换 current 标记);内容拷贝(O(N),仅当缓冲不可原地复用);懒交换(写方只标记 dirty,读者主动拉取时再同步——省一次交换,但引入"滞后"语义)。选择依据:交换频率、缓冲大小、读者能否容忍滞后。
What is the granularity of the buffer?
缓冲粒度:整块缓冲(整个状态一份 next,简单直接,但大状态内存翻倍);按需分块(只有经常变的部分双缓冲,其余共享——省内存,但管理复杂);按实体粒度(每个实体一份双缓冲状态——ECS 常用,局部性好)。粒度越小越省内存,但边界管理越复杂——按实际变化频率选。
See Also
- Game Loop:双缓冲与游戏循环天然配合——循环里"更新世界"写 next,"渲染世界"读 current,swap 放在帧末。
- Update Method:每个实体的 update 写自己的 next 状态,读 current——实体间互不干扰。
- Observer:如果"读者"很多且动态,观察者模式管理"谁在读",双缓冲管理"读什么快照"——两者互补。
可迁移实现或计算骨架
initial state -> 分配双缓冲 -> 写方写 next -> 读者读 current -> swap() 交换角色
fault injection -> 单缓冲读写同一块
pass condition -> 读者始终看到完整帧,交换原子
reset -> initial state对应实现要点:缓冲类持 current/next 两份实例;写接口操作 next,读接口操作 current;swap 交换指针(O(1));按需分块控制内存。该骨架只保存实验合同;真实项目还要固定缓冲大小、交换频率与粒度划分,并保留基线实现以便回退。
本章练习与节点验证矩阵
练习
问题 1:撕裂复现。
操作:用单缓冲模拟"写到一半被读取"(动画中关掉双栏隔离)
问题 2:交换成本对比。
操作:分别实现"交换指针"与"拷贝内容"两种 swap,测耗时
问题 3:粒度实验。
操作:对大状态做整块双缓冲 vs 只对热数据分块双缓冲,测内存
问题 4:AI 一致性。
操作:模拟多个 AI 共享 world 快照决策(current 读、next 写)
术语复核
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 后台缓冲
- 显示缓冲
- 交换
练习答案参考
练习判据即答案:每个练习的"验证判据"列给出了通过标准——先自己动手,再对照判据核验。
术语复核与本章回顾
掌握"8. Double Buffer"意味着能从"单缓冲写哪读哪导致撕裂"出发,解释后台缓冲、显示缓冲、交换与撕裂四者的关系,再用"读者是否始终看到完整帧、交换是否原子、粒度是否合理"三个可观察量推翻或保留实现。若三个判据不能同时满足,本章仍未通过。
一句话回顾:写的那块读者看不见,读者看的那块没人写——中间一道 swap 闸门,读写从此各走各道,撕裂无处可藏。