1160 字
3 分钟
写时复制(Copy-on-Write)
一、为什么需要写时复制
一个配置管理系统中,100 个服务实例共享同一份配置对象。配置更新时,你需要创建新版本,但旧版本仍被正在处理的请求引用。最简单的做法是每次更新都深拷贝整份配置——100 个实例各自持有一份副本。但如果配置有 10MB,那就是 1GB 的内存,而其中 99% 的内容在两次更新间根本没变。
Linux 创建子进程时也会遇到类似问题。fork() 需要让子进程获得父进程内存的副本。如果父进程占用 2GB 内存,逐页复制需要几十毫秒。更糟的是,子进程往往紧接着就调用 exec() 加载新程序,之前复制的页面全部浪费。
写时复制的洞察是:大多数数据被读取的次数远多于被写入的次数。在写操作真正发生之前,所有读者可以安全地共享同一份数据。只有当某个读者需要修改时,才为它创建私有副本。
二、现实类比
一份设为「仅查看」的共享 Google 文档链接。所有人读的都是同一份文档。当有人想编辑时,系统为他创建一份副本。在写操作发生之前,只存在一份。
三、核心思想
写时复制将复制的开销推迟到实际发生修改时。多个读取方共享同一份数据。当写入方需要修改时,系统为该写入方创建副本,其他引用不受影响。
flowchart LR
A[读取方 A] --> D[共享数据]
B[读取方 B] --> D
C[写入方 C] -->|"要修改"| D
D -->|"写时复制"| E[C 的副本]
C --> E
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 读取(共享) | O(1) | 直接引用,无需复制 |
| 写入(首次修改) | O(n) | 完整复制数据 |
| 写入(已拥有) | O(1) | 原地修改 |
| 空间(无写入时) | O(1) | 所有读者共享一份拷贝 |
浅拷贝陷阱:如果 CoW 只做浅拷贝,嵌套对象仍然是共享引用。写者修改嵌套对象会影响所有读者。要实现真正的隔离,需要深拷贝、结构共享或规定 CoW 对象只包含原始类型。
四、变体与对比
| 模式 | 与 CoW 的关系 | 适用场景 |
|---|---|---|
| 双缓冲 | 都延迟成本——CoW 在写入时复制,双缓冲准备第二份副本 | 双缓冲适合读写交替的场景 |
| 享元 | 享元共享不可变数据;CoW 共享可变数据直到修改 | 享元适合完全不可变的共享 |
| 引用计数 | 引用计数追踪 CoW 共享——引用计数 > 1 时写入触发复制 | CoW 的底层依赖引用计数 |
| MVCC | MVCC 使用 CoW 为并发读者创建版本快照 | 数据库并发控制 |
| Merkle 树 | Merkle 树实现高效 CoW——只需重新哈希变更路径 | 内容寻址存储 |
五、多语言实现
5.1 Go 实现
package cow
import "sync/atomic"
// CowSlice 写时复制的切片包装器type CowSlice[T any] struct { data []T shared atomic.Bool}
// Share 从现有切片创建共享的 CoW 引用func Share[T any](data []T) *CowSlice[T] { c := &CowSlice[T]{data: data} c.shared.Store(true) return c}
// Read 读取数据,返回只读引用func (c *CowSlice[T]) Read() []T { return c.data}
// Write 获取可写引用,首次写入时触发复制func (c *CowSlice[T]) Write() []T { if c.shared.Load() { // 共享状态:必须复制 copied := make([]T, len(c.data)) copy(copied, c.data) c.data = copied c.shared.Store(false) } return c.data}
// IsOwned 返回是否独占(已复制或从未共享)func (c *CowSlice[T]) IsOwned() bool { return !c.shared.Load()}使用示例——配置管理:
// 配置更新:旧配置仍在被使用,新配置写时复制func updateConfig(old *cow.CowSlice[ConfigItem], changes []ConfigItem) []ConfigItem { writable := old.Write() // 首次写入触发复制 for _, c := range changes { writable[c.Key] = c } return writable}5.2 TypeScript 实现
class Cow<T extends object> { private data: T; private shared: boolean;
constructor(data: T) { this.data = data; this.shared = false; }
// 从现有数据创建共享引用 static from<T extends object>(data: T): Cow<T> { const cow = new Cow(data); cow.shared = true; return cow; }
// 读取:直接引用,零拷贝 read(): Readonly<T> { return this.data; }
// 写入:共享时深拷贝,否则原地修改 write(): T { if (this.shared) { this.data = structuredClone(this.data); this.shared = false; } return this.data; }
// 是否独占所有权 isOwned(): boolean { return !this.shared; }}
// 使用示例:React 风格的状态更新interface State { users: string[]; count: number;}
const original: State = { users: ["alice", "bob"], count: 2 };const view = Cow.from(original);
// 读取——与 original 共享同一对象console.log(view.read() === original); // true
// 修改——触发深拷贝const mutable = view.write();mutable.users.push("charlie");
// original 不受影响console.log(original.users); // ["alice", "bob"]六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Git | object-file.c#L719-L730 | Git 对象是不可变的内容寻址 blob。分支时不复制文件——共享相同对象,新 commit 只为变更的文件创建新对象 |
| Rust 标准库 | borrow.rs#L169-L220 | Cow<'a, B> 持有 Borrowed 引用或 Owned 值,to_mut() 仅在借用时才克隆。广泛用于零拷贝解析 |
| Linux fork | 内核 copy_page_range | fork() 通过 CoW 页表实现——只复制页表条目并将页面标记为只读,写入时触发缺页中断才复制 |
七、小结
何时使用:
- 读多写少——配置对象、解析后的 AST、缓存响应
- 分支/版本控制——Git 对象模型、数据库快照
- 零拷贝解析——Rust 的
Cow<str>在输入已有效时避免分配 - 不可变优先架构——React state、Redux reducers
何时不用:
- 写多读少——每次写入触发复制,抵消收益
- 小数据——复制小结构比 CoW 记账更便宜
- 并发写入——CoW 不解决并发修改问题,需要额外同步
- 深层结构——浅 CoW 可能导致共享可变子对象的隐性 bug
八、参考资料
- Git 对象模型 - Git 的不可变对象和内容寻址设计
- Rust Cow 文档 - Rust 标准库写时复制类型
- Linux CoW fork - Linux 内核 fork 的写时复制页表实现
- ZFS CoW - ZFS 文件系统基于写时复制的事务模型
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
写时复制(Copy-on-Write)
https://blog.souloss.com/posts/programming/memory/memory-copy-on-write/ 部分信息可能已经过时
相关文章 智能推荐
1
享元(Flyweight)
程序设计 共享不可变的部分,只存储变化的部分——用更少的对象表示更多的数据,游戏引擎和编译器中的经典内存优化。
2
内存映射 mmap(Memory Mapping)
程序设计 文件映射到虚拟地址空间,零拷贝的基础——大文件读取和共享内存的通用机制
3
分代垃圾回收(Generational GC)
程序设计 对象分代,年轻代频繁回收——JVM、V8、Go 运行时的核心内存管理策略
4
Slab 分配器(Slab Allocator)
程序设计 固定大小对象池,消除碎片——Linux 内核和 Memcached 的高频对象分配策略
5
对象池(Object Pool)
程序设计 预分配一组对象反复使用,避免频繁创建和销毁带来的 GC 压力——游戏引擎、网络框架和高性能服务中的经典优化手段。






