mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1160 字
3 分钟
写时复制(Copy-on-Write)
2026-06-13

一、为什么需要写时复制#

一个配置管理系统中,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 的底层依赖引用计数
MVCCMVCC 使用 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"]

六、生产验证#

项目源码位置用途
Gitobject-file.c#L719-L730Git 对象是不可变的内容寻址 blob。分支时不复制文件——共享相同对象,新 commit 只为变更的文件创建新对象
Rust 标准库borrow.rs#L169-L220Cow<'a, B> 持有 Borrowed 引用或 Owned 值,to_mut() 仅在借用时才克隆。广泛用于零拷贝解析
Linux fork内核 copy_page_rangefork() 通过 CoW 页表实现——只复制页表条目并将页面标记为只读,写入时触发缺页中断才复制

七、小结#

何时使用:

  • 读多写少——配置对象、解析后的 AST、缓存响应
  • 分支/版本控制——Git 对象模型、数据库快照
  • 零拷贝解析——Rust 的 Cow<str> 在输入已有效时避免分配
  • 不可变优先架构——React state、Redux reducers

何时不用:

  • 写多读少——每次写入触发复制,抵消收益
  • 小数据——复制小结构比 CoW 记账更便宜
  • 并发写入——CoW 不解决并发修改问题,需要额外同步
  • 深层结构——浅 CoW 可能导致共享可变子对象的隐性 bug

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

写时复制(Copy-on-Write)
https://blog.souloss.com/posts/programming/memory/memory-copy-on-write/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时