• 关于Copyonwrite的理解


    关于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
    }
    
  • 相关阅读:
    CSS3中background-origin和background-clip的区别
    JavaScript的赋值是引用or复制,及参数传递
    写第一个jquery插件实录
    北大acm1008
    北大acm1007
    北大acm1006
    北大acm1005
    北大acm1004
    团队绩效评估
    第二阶段冲刺第十天
  • 原文地址:https://www.cnblogs.com/werben/p/16175228.html
Copyright © 2020-2023  润新知