关于Copy-on-write的理解
定义
写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。Copy-On-Write策略用于读多写少的并发场景。
上面的定义估计第一次看都有点蒙,我这里举一个实际例子,就很好理解了。
代码1
package main
import (
"fmt"
"strconv"
)
type CowMap map[int]*string
func (c *CowMap) Set(key int, value string) {
(*c)[key] = &value
}
func (c *CowMap) Get(key int) string {
return *(*c)[key]
}
func readLoop(c *CowMap) {
for {
fmt.Println(c.Get(3))
}
}
func writeLoop(c *CowMap) {
for i := 0; i < 10000000; i++ {
//修改map的值
c.Set(3, "werben-"+strconv.Itoa(i))
}
}
func main() {
c := make(CowMap)
c.Set(1, "a")
c.Set(2, "b")
c.Set(3, "c")
go readLoop(&c)
writeLoop(&c)
}
运行上面的代码,会出错:fatal error: concurrent map read and map write
因为有两个协程(主协程writeLoop和readLoop协程)同时读写同一个map,而这个map不是线程安全,所以会导致上面的出错。
代码2
为了解决上面的问题我们引入读写锁
package main
import (
"fmt"
"strconv"
"sync"
)
//读写锁
var mu sync.RWMutex
type CowMap map[int]string
func (c *CowMap) Set(key int, value string) {
(*c)[key] = value
}
func (c *CowMap) Get(key int) string {
return (*c)[key]
}
func readLoop(c *CowMap) {
for {
//读的时候上读锁
mu.RLock()
fmt.Println(c.Get(3))
mu.RUnlock()
}
}
func writeLoop(c *CowMap) {
for i := 0; i < 10000000; i++ {
//写的时候上写锁
mu.Lock()
c.Set(3, "werben-"+strconv.Itoa(i))
mu.Unlock()
}
}
func main() {
c := make(CowMap)
c.Set(1, "a")
c.Set(2, "b")
c.Set(3, "c")
go readLoop(&c)
writeLoop(&c)
}
运行上面的代码,不会报错了。
如果我们将writeLoop()改成如下,每5秒写一次。
func writeLoop(c *CowMap) {
for i := 0; i < 10000000; i++ {
//每隔5s写一次
time.Sleep(time.Second*5)
//写的时候上写锁
mu.Lock()
c.Set(3, "werben-"+strconv.Itoa(i))
mu.Unlock()
}
}
我们看下读写锁的特性:
- 读锁不能阻塞读锁
- 读锁需要阻塞写锁,直到所有读锁都释放
- 写锁需要阻塞读锁,直到所有写锁都释放
- 写锁需要阻塞写锁
只是每隔5秒写一次,但是上面的读锁还是一直不断的上锁解锁,这个在没有写数据的时候,其实都是没有意义的。如果时间更长,比如1天才修改一次,读锁浪费了大量的无用资源。
这时候,如果我们用copy-on-write策略,代码如下:
代码3
package main
import (
"fmt"
"strconv"
"time"
)
type CowMap map[int]string
func (c *CowMap) Set(key int, value string) {
(*c)[key] = value
}
func (c *CowMap) Get(key int) string {
return (*c)[key]
}
//拷贝副本函数
func (c *CowMap) Clone() *CowMap {
m := make(CowMap)
for k, v := range *c {
m[k] = v
}
return &m
}
func readLoop(c *CowMap) {
for {
fmt.Println(c.Get(3))
}
}
func writeLoop(c *CowMap) {
for i := 0; i < 10000000; i++ {
//每隔5s写一次
time.Sleep(5 * time.Second)
//写之前,先拷贝一个副本
copy := c.Clone()
//修改副本
copy.Set(3, "werben-"+strconv.Itoa(i))
//修改副本数据后,将副本转正
*c = *copy
}
}
func main() {
c := make(CowMap)
c.Set(1, "a")
c.Set(2, "b")
c.Set(3, "c")
go readLoop(&c)
writeLoop(&c)
}
在写入之前先拷贝一个副本,对副本进行修改,副本修改之后,将副本转正。这时多个调用者只是读取时就可以不需要上锁了。
缺点
内存占用问题
因为Copy-On-Write的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。
数据一致性问题
Copy-On-Write策略只能保证数据的最终一致性,不能保证数据的实时一致性。写入数据之后,不能保证马上读取到最新的数据。
实际应用
我们来看下在golang官方库btree里是怎么使用Copy-On-Write策略的
这个库的官方地址在这里,有兴趣的可以去读一读:https://github.com/google/btree
首先在BTree定义里,定义了一个copyOnWriteContext, cow里实际就是保存了一个*node的数组,
然后每个node里面也保存了一份cow。
type BTree struct {
degree int
length int
root *node
cow *copyOnWriteContext
}
...
type copyOnWriteContext struct {
freelist *FreeList
}
...
type FreeList struct {
mu sync.Mutex
freelist []*node
}
type node struct {
items items
children children
cow *copyOnWriteContext
}
BTree有一个Clone方法, 可以看到虽然clone出来一个新的cow2,但是cow1和cow2里指向的freelist却还是都是同一个地址(freelist)。也就是如果只是读取Clone()出来的新BTree,跟原先的是同一份freelist,freelist里面的node如果不修改,则都可以不需要重新拷贝。
func (t *BTree) Clone() (t2 *BTree) {
// Create two entirely new copy-on-write contexts.
// This operation effectively creates three trees:
// the original, shared nodes (old b.cow)
// the new b.cow nodes
// the new out.cow nodes
cow1, cow2 := *t.cow, *t.cow
out := *t
t.cow = &cow1
out.cow = &cow2
return &out
}
当需要修改freelist里面的Node的时候
func (t *BTree) ReplaceOrInsert(item Item) Item {
if item == nil {
panic("nil item being added to BTree")
}
if t.root == nil {
t.root = t.cow.newNode()
t.root.items = append(t.root.items, item)
t.length++
return nil
} else {
//这里判断一下需不需要重新拷贝node
t.root = t.root.mutableFor(t.cow)
if len(t.root.items) >= t.maxItems() {
item2, second := t.root.split(t.maxItems() / 2)
oldroot := t.root
t.root = t.cow.newNode()
t.root.items = append(t.root.items, item2)
t.root.children = append(t.root.children, oldroot, second)
}
}
out := t.root.insert(item, t.maxItems())
if out == nil {
t.length++
}
return out
}
//这里的逻辑就是判断一下,是否需要重新复制node,并修改其值
func (n *node) mutableFor(cow *copyOnWriteContext) *node {
//判断一下,当前node的cow和BTree里面的cow是不是同一个
//如果不是Clone()出来的BTree,则不涉及到拷贝,直接返回当前node
if n.cow == cow {
//如果不是Clone()出来的BTree,则不涉及到拷贝,
//直接返回当前node,
return n
}
//如果是Clone()出来的新BTree,新的BTree里的cow是变动了的,
//说明这个node的数据才需要重新拷贝一份。
out := cow.newNode()
if cap(out.items) >= len(n.items) {
out.items = out.items[:len(n.items)]
} else {
out.items = make(items, len(n.items), cap(n.items))
}
copy(out.items, n.items)
// Copy children
if cap(out.children) >= len(n.children) {
out.children = out.children[:len(n.children)]
} else {
out.children = make(children, len(n.children), cap(n.children))
}
copy(out.children, n.children)
return out
}