定义
写入时复制(英语:Copy-On-Write
,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。Copy-On-Write策略用于读多写少的并发场景。
上面的定义估计第一次看都有点蒙,我这里举一个实际例子,就很好理解了。
代码1 - 并发出错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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++ { 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-引入读写锁
为了解决上面的问题我们引入读写锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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++ { time.Sleep(time.Second*5) mu.Lock() c.Set(3, "werben-"+strconv.Itoa(i)) mu.Unlock() } }
|
我们看下读写锁的特性:
- 读锁不能阻塞读锁
- 读锁需要阻塞写锁,直到所有读锁都释放
- 写锁需要阻塞读锁,直到所有写锁都释放
- 写锁需要阻塞写锁
只是每隔5秒写一次,但是上面的读锁还是一直不断的上锁解锁,这个在没有写数据的时候,其实都是没有意义的。如果时间更长,比如1天才修改一次,读锁浪费了大量的无用资源。
这时候,如果我们用copy-on-write策略,代码如下:
代码3 - Copy-On-Write实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| package main
import ( "fmt" "strconv" "sync" "sync/atomic" )
type tmap map[int]string
type CowMap struct { mu sync.Mutex data atomic.Value }
func NewCowMap() *CowMap { m := make(tmap) c := &CowMap{} c.data.Store(m) return c }
func (c *CowMap) clone() tmap { m := make(tmap) for k, v := range c.data.Load().(tmap) { m[k] = v } return m }
func (c *CowMap) Get(key int) (value string, ok bool) { value, ok = c.data.Load().(tmap)[key] return }
func (c *CowMap) Set(key int, value string) { c.mu.Lock() defer c.mu.Unlock()
copy := c.clone() copy[key] = value c.data.Store(copy) }
func readLoop(c *CowMap) { for { fmt.Println(c.Get(3)) } }
func writeLoop(c *CowMap) { for i := 0; i < 10000000; i++ { c.Set(3, "werben-"+strconv.Itoa(i)) } }
func main() { c := NewCowMap() 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| 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) { 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 { 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 }
func (n *node) mutableFor(cow *copyOnWriteContext) *node { if n.cow == cow { return n }
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) 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 }
|