1. 程式人生 > 其它 >關於Copy-on-write的理解

關於Copy-on-write的理解

關於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容器只能保證資料的最終一致性,不能保證資料的實時一致性。寫入資料之後,不能保證馬上讀取到最新的資料。