1. 程式人生 > >深入理解golang:sync.map

深入理解golang:sync.map

## 疑惑開篇 有了map為什麼還要搞個sync.map 呢?它們之間有什麼區別? 答:重要的一點是,map併發不是安全的。 >在Go 1.6之前, 內建的map型別是部分goroutine安全的,併發的讀沒有問題,併發的寫可能有問題。自go 1.6之後, 併發地讀寫map會報錯,這在一些知名的開源庫中都存在這個問題,所以go 1.9之前的解決方案是額外繫結一個鎖,封裝成一個新的struct或者單獨使用鎖都可以。 >go version go1.13.9 windows/amd64 ### 測試一波 寫一個簡單的併發寫map的測試程式碼看看: testcurmap.go ```Go package main import ( "fmt" "time" ) func main() { m := map[string]int{"age": 10} go func() { i := 0 for i < 1000 { m["age"] = 10 i++ } }() go func() { //19 行 i := 0 for i < 1000 { m["age"] = 11 //22 行 i++ } }() time.Sleep(time.Second * 3) fmt.Println(m) } ``` 多執行幾次:go run testcurmap.go 會報錯,錯誤的扼要資訊如下: >
fatal error: concurrent map writes > >goroutine 7 [running]: >runtime.throw(0x4d49a3, 0x15) >       /go/src/runtime/panic.go:774 +0x79 fp=0xc000041f30 sp=0xc000041f00 pc=0x42cf19 >runtime.mapassign_faststr(0x4b4360, 0xc000066330, 0x4d168a, 0x3, 0x0) >        /go/src/runtime/map_faststr.go:211 +0x41e fp=0xc000041f98 sp=0xc000041f30 pc=0x410f8e >main.main.func2(0xc000066330) >
        /mygo/src/study/go-practice2/map/curmap/testcurmap.go:22 +0x5c fp=0xc000041fd8 sp=0xc000041f98 pc=0x49ac9c >runtime.goexit() >      /go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc000041fe0 sp=0xc000041fd8 pc=0x455391 >created by main.main >       /mygo/src/study/go-practice2/map/curmap/testcurmap.go:19 +0xb0 > >exit status 2 看報錯資訊是src/runtime/map_faststr.go:211 這個函式runtime.mapassign_faststr,它在runtime/map_faststr.go 中,簡要程式碼如下: ```Go func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer { ... ... if h.flags&hashWriting != 0 { throw("concurrent map writes") } ... ... } ``` hashWriting  = 4 // a goroutine is writing to the map goroutine寫的一個標識, 這裡h.flags與自己進行與運算,判斷是否有其他goroutine在操作這個map,不是0說明有其他goroutine操作map,所以報錯。 那咋防止map併發呢,一般有幾種方式: 1. map+Mutex: 給map加一把大鎖 2. map+RWMutex 給map加一個讀寫鎖,給鎖細分。適合讀多寫少場景 #### 修改一下程式 加一把讀寫鎖防止併發,修改程式 testcurmap2.go: ```Go package main import ( "fmt" "sync" "time" ) func main() { m := map[string]int{"age": 10} var s sync.RWMutex go func() { i := 0 for i < 1000 { s.Lock() m["age"] = 10 s.Unlock() i++ } }() go func() { i := 0 for i < 1000 { s.Lock() m["age"] = 11 s.Unlock() i++ } }() time.Sleep(time.Second * 3) fmt.Println(m) } ``` 執行結果: map[age:11] 沒有報錯了。 就到這裡了嗎?可以在思考思考,還有其他方法控制併發的方法沒?有的,sync.map 登場 控制並的第三種方式: 3. sync.Map 官方實現的併發map。 原理是通過分離讀寫map和原子指令來實現讀的近似無鎖,並通過延遲更新的方式來保證讀的無鎖化。一般情況下可以替換上面2種鎖。 ## sync.map 先看一個簡單的程式碼 testcurmap3.go ```Go package main import ( "fmt" "sync" "time" ) func main() { smap := sync.Map{} smap.Store("age", 10) go func() { i := 0 for i < 1000 { smap.Store("one", 10) i++ } }() go func() { i := 0 for i < 1000 { smap.Store("one", 11) i++ } }() time.Sleep(time.Second * 2) fmt.Println(smap.Load("one")) } ``` 執行輸出:11 true 正常輸出,沒有報錯。 **sync.Map 的主要思想就是讀寫分離,空間換時間**。 看看 sync.map 優點: 1. 空間換時間:通過冗餘的兩個資料結構(read、dirty),實現加鎖對效能的影響。 2. 使用只讀資料(read),避免讀寫衝突。 3. 動態調整,miss次數多了之後,將dirty資料遷移到read中。 4. double-checking。 5. 延遲刪除。 刪除一個鍵值只是打標記,只有在遷移dirty資料的時候才清理刪除的資料。 6. 優先從read讀取、更新、刪除,因為對read的讀取不需要鎖。 ### sync.Map 資料結構 #### Map 資料結構 在 src/sync/map.go 中 ```Go type Map struct { // 當涉及到髒資料(dirty)操作時候,需要使用這個鎖     mu Mutex // read是一個只讀資料結構,包含一個map結構, // 讀不需要加鎖,只需要通過 atomic 載入最新的指正即可     read atomic.Value // readOnly // dirty 包含部分map的鍵值對,如果操作需要mutex獲取鎖 // 最後dirty中的元素會被全部提升到read裡的map去     dirty map[interface{}]*entry // misses是一個計數器,用於記錄read中沒有的資料而在dirty中有的資料的數量。 // 也就是說如果read不包含這個資料,會從dirty中讀取,並misses+1 // 當misses的數量等於dirty的長度,就會將dirty中的資料遷移到read中     misses int } ``` #### read的資料結構 readOnly: ```Go // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { // m包含所有隻讀資料,不會進行任何的資料增加和刪除操作 // 但是可以修改entry的指標因為這個不會導致map的元素移動     m       map[interface{}]*entry // 標誌位,如果為true則表明當前read只讀map的資料不完整,dirty map中包含部分資料     amended bool // true if the dirty map contains some key not in m. } ``` 只讀map,對該map的訪問不需要加鎖,但是這個map也不會增加元素,元素會被先增加到dirty中,然後後續會遷移到read只讀map中,通過原子操作所以不需要加鎖操作。 #### entry readOnly.m和Map.dirty儲存的值型別是*entry,它包含一個指標p, 指向使用者儲存的value值,結構如下: ```Go type entry struct { p unsafe.Pointer // *interface{} } ``` p有三種值: - nil: entry已被刪除了,並且m.dirty為nil - expunged: entry已被刪除了,並且m.dirty不為nil,而且這個entry不存在於m.dirty中 - 其它: entry是一個正常的值 ## 查詢 根據key來查詢 value, 函式為 Load(),原始碼如下: ```Go // src/sync/map.go // Load returns the value stored in the map for a key, or nil if no // value is present. // The ok result indicates whether value was found in the map. func (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 首先從只讀ready的map中查詢,這時不需要加鎖     read, _ := m.read.Load().(readOnly)     e, ok := read.m[key] // 如果沒有找到,並且read.amended為true,說明dirty中有新資料,從dirty中查詢,開始加鎖了     if !ok && read.amended {         m.mu.Lock() // 加鎖          // 又在 readonly 中檢查一遍,因為在加鎖的時候 dirty 的資料可能已經遷移到了read中         read, _ = m.read.Load().(readOnly)         e, ok = read.m[key] // read 還沒有找到,並且dirty中有資料         if !ok && read.amended {             e, ok = m.dirty[key] //從 dirty 中查詢資料              // 不管m.dirty中存不存在,都將misses + 1 // missLocked() 中滿足條件後就會把m.dirty中資料遷移到m.read中             m.missLocked()         }         m.mu.Unlock()     }     if !ok {         return nil, false     }     return e.load() } ``` 從函式可以看出,如果查詢的鍵值正好在m.read中,不需要加鎖,直接返回結果,優化了效能。 即使不在read中,經過幾次miss後, m.dirty中的資料也會遷移到m.read中,這時又可以從read中查詢。 所以對於更新/增加較少,載入存在的key很多的case,效能基本和無鎖的map類似。 **missLockerd() 遷移資料:** ```Go // src/sync/map.go func (m *Map) missLocked() {     m.misses++     if m.misses < len(m.dirty) {//misses次數小於 dirty的長度,就不遷移資料,直接返回         return     }     m.read.Store(readOnly{m: m.dirty}) //開始遷移資料     m.dirty = nil //遷移完dirty就賦值為nil     m.misses = 0 //遷移完 misses歸0 } ``` ## 新增和更新 方法是 Store(), 更新或者新增一個 entry, 原始碼如下: ```Go // src/sync/map.go // Store sets the value for a key. func (m *Map) Store(key, value interface{}) { // 直接在read中查詢值,找到了,就嘗試 tryStore() 更新值     read, _ := m.read.Load().(readOnly)     if e, ok := read.m[key]; ok && e.tryStore(&value) {         return     } // m.read 中不存在     m.mu.Lock()     read, _ = m.read.Load().(readOnly)     if e, ok := read.m[key]; ok {         if e.unexpungeLocked() { // 未被標記成刪除,前面講到entry資料結構時,裡面的p值有3種。1.nil 2.expunged,這個值含義有點複雜,可以看看前面entry資料結構 3.正常值                          m.dirty[key] = e // 加入到dirty裡         }         e.storeLocked(&value) // 更新值     } else if e, ok := m.dirty[key]; ok { // 存在於 dirty 中,直接更新         e.storeLocked(&value)     } else { // 新的值         if !read.amended { // m.dirty 中沒有新資料,增加到 m.dirty 中             // We're adding the first new key to the dirty map.             // Make sure it is allocated and mark the read-only map as incomplete.             m.dirtyLocked() // 從 m.read中複製未刪除的資料             m.read.Store(readOnly{m: read.m, amended: true})         }         m.dirty[key] = newEntry(value) //將這個entry加入到m.dirty中     }     m.mu.Unlock() } ``` 操作都是先從m.read開始,不滿足條件再加鎖,然後操作m.dirty。 ## 刪除 根據key刪除一個值: ```Go // src/sync/map.go // Delete deletes the value for a key. func (m *Map) Delete(key interface{}) { // 從 m.read 中開始查詢     read, _ := m.read.Load().(readOnly)     e, ok := read.m[key]     if !ok && read.amended { // m.read中沒有找到,並且可能存在於m.dirty中,加鎖查詢         m.mu.Lock() // 加鎖         read, _ = m.read.Load().(readOnly) // 再在m.read中查詢一次         e, ok = read.m[key]         if !ok && read.amended { //m.read中又沒找到,amended標誌位true,說明在m.dirty中             delete(m.dirty, key) // 刪除         }         m.mu.Unlock()     }     if ok { // 在 m.ready 中就直接刪除         e.delete()     } } ``` 還有更好的方法沒?java裡面有一個分段鎖,保證在操作不同 map 段的時候, 可以併發執行, 操作同段 map 的時候,進行鎖的競爭和等待。從而達到執行緒安全, 且效率大於 synchronized。而不是直接加一把大鎖,鎖住整個map。 那go裡面有木有?有人已經想到了 ## concurrent-map 專案地址:[concurrent-map](https://github.com/orcaman/concurrent-map) 中文wiki:[地址](https://github.com/orcaman/concurrent-map/blob/master/README-zh.md) 正如 [這裡](http://golang.org/doc/faq#atomic_maps) 和 [這裡](http://blog.golang.org/go-maps-in-action) 所描述的, Go語言原生的map型別並不支援併發讀寫。concurrent-map提供了一種高效能的解決方案:通過對內部map進行分片,降低鎖粒度,從而達到最少的鎖等待時間(鎖衝突)。 在Go 1.9之前,go語言標準庫中並沒有實現併發map。在Go 1.9中,引入了sync.Map。新的sync.Map與此concurrent-map有幾個關鍵區別。標準庫中的sync.Map是專為append-only場景設計的。因此,如果您想將Map用於一個類似記憶體資料庫,那麼使用我們的版本可能會受益。你可以在golang repo上讀到更多,[這裡](https://github.com/golang/go/issues/21035) and [這裡](https://stackoverflow.com/questions/11063473/map-with-concurrent-access) >
譯註: sync.Map在讀多寫少效能比較好,否則併發效能很差 有興趣的可以自己研究下。 ## 參考: - [Go 1.9 sync.Map揭祕](https://colobu.com/2017/07/11/dive-into-sync-Map/) - [go sync.Map使用和介紹](https://blog.csdn.net/u010230794/article/details/82143179) - [golang sync.map原始碼](https://github.com/golang/go/blob/master/src/sync/