hi, 你們好,我是 haohongfan。git
本篇文章會從使用方式和源碼角度剖析 sync.Map。不過不論是平常開發仍是開源項目中,好像 sync.Map 並無獲得很好的利用,你們仍是習慣使用 Mutex + Map 來使用。github
下面這段代碼,看起來頗有道理,實際上是用錯了(背景:併發場景中獲取註冊信息)。golang
instance, ok := instanceMap[name]
if ok {
return instance, nil
}
initLock.Lock()
defer initLock.Unlock()
// double check
instance, ok = instanceMap[name]
if ok {
return instance, nil
}
這裏使用使用 sync.Map 會更合理些,由於 sync.Map 底層徹底包含了這個邏輯。可能寫 Java 的同窗看着上面這段代碼很眼熟,但確實是用錯了,關於爲何用錯了以及會形成什麼影響,請你們關注後續的文章。微信
我大概分析了下你們寧願使用 Mutex + Map,也不肯使用 sync.Map 的緣由:併發
-
sync.Map 自己就很難用,使用起來並不像一個 Map。失去了 map 應有的特權語法,如:make, map[1] 等函數
-
sync.Map 方法較多。讓一個簡單的 Map 使用起來有了較高的學習成本。性能
無論什麼樣的緣由吧,當你讀過這篇文章後,在某些特定的併發場景下,建議使用 sync.Map 代替 Map + Mutex 的。學習
用法全解
package main import ( "fmt" "sync" ) func main() { var syncMap sync.Map syncMap.Store("11", 11) syncMap.Store("22", 22) fmt.Println(syncMap.Load("11")) // 11 fmt.Println(syncMap.Load("33")) // 空 fmt.Println(syncMap.LoadOrStore("33", 33)) // 33 fmt.Println(syncMap.Load("33")) // 33 fmt.Println(syncMap.LoadAndDelete("33")) // 33 fmt.Println(syncMap.Load("33")) // 空 syncMap.Range(func(key, value interface{}) bool { fmt.Printf("key:%v value:%v\n", key, value) return true }) // key:22 value:22 // key:11 value:11 }
其實 sync.Map 並不複雜,只是將普通 map 的相關操做轉成對應函數而已。優化
普通 map | sync.Map | |
---|---|---|
map 獲取某個 key | map[1] | sync.Load(1) |
map 添加元素 | map[1] = 10 | sync.Store(1, 10) |
map 刪除一個 key | delete(map, 1) | sync.Delete(1) |
遍歷 map | for...range | sync.Range() |
sync.Map 兩個特有的函數,不過從字面就能理解是什麼意思了。LoadOrStore:sync.Map 存在就返回,不存在就插入 LoadAndDelete:sync.Map 獲取某個 key,若是存在的話,同時刪除這個 keyatom
源碼解析
type Map struct { mu Mutex read atomic.Value // readOnly read map dirty map[interface{}]*entry // dirty map misses int }

sync map 全景圖

Load

Store

Delete
read map 的值是什麼時間更新的 ?
-
Load/LoadOrStore/LoadAndDelete 時,當 misses 數量大於等於 dirty map 的元素個數時,會總體複製 dirty map 到 read map
-
Store/LoadOrStore 時,當 read map 中存在這個key,則更新
-
Delete/LoadAndDelete 時,若是 read map 中存在這個key,則設置這個值爲 nil
dirty map 的值是什麼時間更新的 ?
-
徹底是一個新 key, 第一次插入 sync.Map,必先插入 dirty map
-
Store/LoadOrStore 時,當 read map 中不存在這個key,在 dirty map 存在這個key,則更新
-
Delete/LoadAndDelete 時,若是 read map 中不存在這個key,在 dirty map 存在這個key,則從 dirty map 中刪除這個key
-
當 misses 數量大於等於 dirty map 的元素個數時,會總體複製 dirty map 到 read map,同時設置 dirty map 爲 nil
疑問:當 dirty map 複製到 read map 後,將 dirty map 設置爲 nil,也就是 dirty map 中就不存在這個 key 了。若是又新插入某個 key,屢次訪問後達到了 dirty map 往 read map 複製的條件,若是直接用 read map 覆蓋 dirty map,那豈不是就丟了以前在 read map 但不在 dirty map 的 key ?
答:其實並不會。當 dirty map 向 read map 複製後,readOnly.amended 等於了 false。當新插入了一個值時,會將 read map 中的值,從新給 dirty map 賦值一遍,也就是 read map 也會向 dirty map 中複製。
func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } } }
read map 和 dirty map 是什麼時間刪除的?
-
當 read map 中存在某個 key 的時候,這個時候只會刪除 read map, 並不會刪除 dirty map(由於 dirty map 不存在這個值)
-
當 read map 中不存在時,纔會去刪除 dirty map 裏面的值
疑問:若是按照這個刪除方式,那豈不是 dirty map 中會有殘餘的 key,致使沒刪除掉?
答:其實並不會。當 misses 數量大於等於 dirty map 的元素個數時,會總體複製 dirty map 到 read map。這個過程當中還附帶了另一個操做:將 dirty map 置爲 nil。
func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
read map 與 dirty map 的關係 ?
-
在 read map 中存在的值,在 dirty map 中可能不存在。
-
在 dirty map 中存在的值,在 read map 中也可能存在。
-
當訪問屢次,發現 dirty map 中存在,read map 中不存在,致使 misses 數量大於等於 dirty map 的元素個數時,會總體複製 dirty map 到 read map。
-
當出現 dirty map 向 read map 複製後,dirty map 會被置成 nil。
-
當出現 dirty map 向 read map 複製後,readOnly.amended 等於了 false。當新插入了一個值時,會將 read map 中的值,從新給 dirty map 賦值一遍
read/dirty map 中的值必定是有效的嗎?
並不必定。放入到 read/dirty map 中的值總共有 3 種類型:
-
nil:若是獲取到的 value 是 nil,那說明這個 key 是已經刪除過的。既不在 read map,也不在 dirty map
-
expunged:這個 key 在 dirty map 中是不存在的
-
valid:其實就正常的狀況,要麼這個值存在在 read map 中,要麼存在在 dirty map 中
sync.Map 是如何提升性能的?
經過源碼解析,咱們知道 sync.Map 裏面有兩個普通 map,read map主要是負責讀,dirty map 是負責讀和寫(加鎖)。在讀多寫少的場景下,read map 的值基本不發生變化,可讓 read map 作到無鎖操做,就減小了使用 Mutex + Map 必須的加鎖/解鎖環節,所以也就提升了性能。
不過也可以看出來,read map 也是會發生變化的,若是某些 key 寫操做特別頻繁的話,sync.Map 基本也就退化成了 Mutex + Map(有可能性能還不如 Mutex + Map)。
因此,不是說使用了 sync.Map 就必定能提升程序性能,咱們平常使用中儘可能注意拆分粒度來使用 sync.Map。
關於如何分析 sync.Map 是否優化了程序性能,一樣可使用 pprof。具體過程能夠參考 《這多是最容易理解的 Go Mutex 源碼剖析》
sync.Map 應用場景
-
讀多寫少
-
寫操做也多,可是修改的 key 和讀取的 key 特別不重合。
關於第二點我以爲挺扯的,畢竟咱們很難把控這一點,不過因爲是官方的註釋仍是放在這裏。
實際開發中咱們要注意使用場景和擅用 pprof 來分析程序性能。
sync.Map 使用注意點
和 Mutex 同樣, sync.Map 也一樣不能被複制,由於 atomic.Value 是不能被複制的。
參考連接
-
https://golang.design/under-the-hood/zh-cn/part1basic/ch05sync/map/
-
https://draveness.me/golang-sync-primitives/
-
https://github.com/golang/go/blob/master/src/sync/map.go
sync.Map 的剖析到這裏基本就結束了。有什麼想跟我交流的,歡迎評論區留言。
歡迎關注個人公衆號,隨時關注個人動態
sync.Map 完整流程圖獲取連接:連接: https://pan.baidu.com/s/16yEnZFbXwSe3qkvX1zi-Wg 密碼: 8w8k。其餘模塊流程圖,請關注公衆號回覆 1 獲取。
學習資料分享,關注公衆號回覆指令:
-
回覆 0,獲取 《Go 面經》
-
回覆 1,獲取 《Go 源碼流程圖》
本文分享自微信公衆號 - HHFCodeRv(hhfcodearts)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。