我想知道什么是写时复制以及它的用途。Sun JDK 教程中多次提到该术语。
9 回答
我打算写我自己的解释,但这篇维基百科文章几乎总结了它。
这是基本概念:
Copy-on-write(有时称为“COW”)是计算机编程中使用的一种优化策略。基本思想是,如果多个调用者请求最初无法区分的资源,您可以给他们指向同一资源的指针。这个函数可以一直保持到调用者试图修改其资源的“副本”,此时会创建一个真正的私有副本以防止更改对其他人可见。所有这些都对调用者透明地发生。主要优点是,如果调用者从不进行任何修改,则不需要创建私有副本。
这里还有一个常用的 COW 应用程序:
COW 概念还用于维护数据库服务器(如 Microsoft SQL Server 2005)上的即时快照。即时快照通过在更新底层数据时存储数据的修改前副本来保留数据库的静态视图。即时快照用于测试用途或与时刻相关的报告,不应用于替换备份。
“写入时复制”或多或少的意思是:每个人都拥有相同数据的一个共享副本,直到它被写入,然后复制。通常,写时复制用于解决并发问题。例如,在ZFS中,磁盘上的数据块被分配写时复制;只要没有变化,你就保留原来的块;更改仅更改了受影响的块。这意味着分配了最少数量的新块。
这些更改通常也被实现为事务性的,即它们具有ACID属性。这消除了一些并发问题,因为这样可以保证所有更新都是原子的。
我不会在 Copy-on-Write 上重复相同的答案。我认为安德鲁的回答和查理的回答已经说得很清楚了。我会给你一个来自操作系统世界的例子,只是提到这个概念的使用范围有多广。
我们可以使用fork()
或vfork()
创建一个新的进程。vfork 遵循写时复制的概念。例如,vfork 创建的子进程会与父进程共享数据和代码段。这加快了分叉时间。如果您在执行 exec 后执行 vfork,则应使用 vfork。因此 vfork 将创建子进程,该子进程将与其父进程共享数据和代码段,但是当我们调用 exec 时,它将在子进程的地址空间中加载新可执行文件的映像。
再举一个例子,Mercurial 使用写时复制使克隆本地存储库成为一项真正“便宜”的操作。
原理与其他示例相同,只是您谈论的是物理文件而不是内存中的对象。最初,克隆不是复制品,而是与原件的硬链接。当您更改克隆中的文件时,会写入副本以表示新版本。
Erich Gamma 等人的《设计模式:可重用的面向对象软件的元素》一书。清楚地描述了写时复制优化(“后果”部分,“代理”一章):
代理模式在访问对象时引入了一定程度的间接性。附加间接有很多用途,具体取决于代理的类型:
- 远程代理可以隐藏对象驻留在不同地址空间的事实。
- 虚拟代理可以执行优化,例如按需创建对象。
- 保护代理和智能引用都允许在访问对象时执行额外的内务处理任务。
代理模式可以对客户端隐藏另一个优化。它被称为写时复制,它与按需创建有关。复制一个大而复杂的对象可能是一项昂贵的操作。如果从不修改副本,则无需产生此费用。通过使用代理来推迟复制过程,我们确保只有在对象被修改时才支付复制对象的代价。
要使写时复制工作,必须对主题进行引用计数。复制代理只会增加这个引用计数。只有当客户端请求修改主题的操作时,代理才会真正复制它。在这种情况下,代理还必须减少主题的引用计数。当引用计数变为零时,主题将被删除。
Copy-on-write 可以显着降低复制重量级主题的成本。
下面是使用代理模式的写时复制优化的 Python 实现。这种设计模式的目的是为另一个对象提供一个代理来控制对它的访问。
代理模式的类图:
Proxy模式的对象图:
首先我们定义主题的接口:
import abc
class Subject(abc.ABC):
@abc.abstractmethod
def clone(self):
raise NotImplementedError
@abc.abstractmethod
def read(self):
raise NotImplementedError
@abc.abstractmethod
def write(self, data):
raise NotImplementedError
接下来我们定义实现主题接口的真实主题:
import copy
class RealSubject(Subject):
def __init__(self, data):
self.data = data
def clone(self):
return copy.deepcopy(self)
def read(self):
return self.data
def write(self, data):
self.data = data
最后我们定义实现主题接口并引用真实主题的代理:
class Proxy(Subject):
def __init__(self, subject):
self.subject = subject
try:
self.subject.counter += 1
except AttributeError:
self.subject.counter = 1
def clone(self):
return Proxy(self.subject) # attribute sharing (shallow copy)
def read(self):
return self.subject.read()
def write(self, data):
if self.subject.counter > 1:
self.subject.counter -= 1
self.subject = self.subject.clone() # attribute copying (deep copy)
self.subject.counter = 1
self.subject.write(data)
然后,客户端可以通过使用代理作为真实主题的替身,从写时复制优化中受益:
if __name__ == '__main__':
x = Proxy(RealSubject('foo'))
x.write('bar')
y = x.clone() # the real subject is shared instead of being copied
print(x.read(), y.read()) # bar bar
assert x.subject is y.subject
x.write('baz') # the real subject is copied on write because it was shared
print(x.read(), y.read()) # baz bar
assert x.subject is not y.subject
我发现这篇关于 PHP 中 zval 的好文章也提到了 COW:
Copy On Write(缩写为“COW”)是一种旨在节省内存的技巧。它更普遍地用于软件工程。这意味着当你写入一个符号时,PHP 将复制内存(或分配新的内存区域),如果这个符号已经指向一个 zval。
它还用于 Ruby“企业版”作为节省内存的巧妙方法。
一个很好的例子是 Git,它使用一种策略来存储 blob。为什么它使用哈希?部分是因为这些更容易执行差异,但也因为使优化 COW 策略更简单。当您使用少量文件更改进行新提交时,绝大多数对象和树都不会更改。因此,提交时,会通过各种由哈希组成的指针引用一堆已经存在的对象,使得存储整个历史所需的存储空间更小。
这是一个内存保护概念。在这个编译器中创建额外的副本来修改子数据,并且这个更新的数据不会反映在父数据中。