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