Go 的內建 map
是不支持併發寫操做的,緣由是 map
寫操做不是併發安全的,當你嘗試多個 Goroutine 操做同一個 map
,會產生報錯:fatal error: concurrent map writes
。git
所以官方另外引入了 sync.Map
來知足併發編程中的應用。github
sync.Map
的實現原理可歸納爲:golang
Map
的數據結構以下:編程
type Map struct { // 加鎖做用,保護 dirty 字段 mu Mutex // 只讀的數據,實際數據類型爲 readOnly read atomic.Value // 最新寫入的數據 dirty map[interface{}]*entry // 計數器,每次須要讀 dirty 則 +1 misses int }
其中 readOnly 的數據結構爲:安全
type readOnly struct { // 內建 map m map[interface{}]*entry // 表示 dirty 裏存在 read 裏沒有的 key,經過該字段決定是否加鎖讀 dirty amended bool }
entry
數據結構則用於存儲值的指針:數據結構
type entry struct { p unsafe.Pointer // 等同於 *interface{} }
屬性 p 有三種狀態:併發
p == nil
: 鍵值已經被刪除,且 m.dirty == nil
p == expunged
: 鍵值已經被刪除,但 m.dirty!=nil
且 m.dirty
不存在該鍵值(expunged 實際是空接口指針)m.read.m
中,若是 m.dirty!=nil
則也存在於 m.dirty
Map
經常使用的有如下方法:函數
Load
:讀取指定 key 返回 valueStore
: 存儲(增或改)key-valueDelete
: 刪除指定 keyfunc (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 首先嚐試從 read 中讀取 readOnly 對象 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // 若是不存在則嘗試從 dirty 中獲取 if !ok && read.amended { m.mu.Lock() // 因爲上面 read 獲取沒有加鎖,爲了安全再檢查一次 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 確實不存在則從 dirty 獲取 if !ok && read.amended { e, ok = m.dirty[key] // 調用 miss 的邏輯 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } // 從 entry.p 讀取值 return e.load() } func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } // 當 miss 積累過多,會將 dirty 存入 read,而後 將 amended = false,且 m.dirty = nil m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
func (m *Map) Store(key, value interface{}) { read, _ := m.read.Load().(readOnly) // 若是 read 裏存在,則嘗試存到 entry 裏 if e, ok := read.m[key]; ok && e.tryStore(&value) { return } // 若是上一步沒執行成功,則要分狀況處理 m.mu.Lock() read, _ = m.read.Load().(readOnly) // 和 Load 同樣,從新從 read 獲取一次 if e, ok := read.m[key]; ok { // 狀況 1:read 裏存在 if e.unexpungeLocked() { // 若是 p == expunged,則須要先將 entry 賦值給 dirty(由於 expunged 數據不會留在 dirty) m.dirty[key] = e } // 用值更新 entry e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { // 狀況 2:read 裏不存在,但 dirty 裏存在,則用值更新 entry e.storeLocked(&value) } else { // 狀況 3:read 和 dirty 裏都不存在 if !read.amended { // 若是 amended == false,則調用 dirtyLocked 將 read 拷貝到 dirty(除了被標記刪除的數據) m.dirtyLocked() // 而後將 amended 改成 true m.read.Store(readOnly{m: read.m, amended: true}) } // 將新的鍵值存入 dirty m.dirty[key] = newEntry(value) } m.mu.Unlock() } func (e *entry) tryStore(i *interface{}) bool { for { p := atomic.LoadPointer(&e.p) if p == expunged { return false } if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { return true } } } func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(&e.p, expunged, nil) } func (e *entry) storeLocked(i *interface{}) { atomic.StorePointer(&e.p, unsafe.Pointer(i)) } 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 { // 判斷 entry 是否被刪除,不然就存到 dirty 中 if !e.tryExpungeLocked() { m.dirty[k] = e } } } func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { // 若是有 p == nil(即鍵值對被 delete),則會在這個時機被置爲 expunged if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
func (m *Map) Delete(key interface{}) { m.LoadAndDelete(key) } // LoadAndDelete 做用等同於 Delete,而且會返回值與是否存在 func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) { // 獲取邏輯和 Load 相似,read 不存在則查詢 dirty read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] m.missLocked() } m.mu.Unlock() } // 查詢到 entry 後執行刪除 if ok { // 將 entry.p 標記爲 nil,數據並無實際刪除 // 真正刪除數據並被被置爲 expunged,是在 Store 的 tryExpungeLocked 中 return e.delete() } return nil, false }
可見,經過這種讀寫分離的設計,解決了併發狀況的寫入安全,又使讀取速度在大部分狀況能夠接近內建 map
,很是適合讀多寫少的狀況。atom
sync.Map
還有一些其餘方法:設計
Range
:遍歷全部鍵值對,參數是回調函數LoadOrStore
:讀取數據,若不存在則保存再讀取這裏就再也不詳解了,可參見 源碼。