前言 在併發編程中,我們經常會遇到多個goroutine同時操作一個map的情況。如果在這種情況下直接使用普通的map,那麼就可能會引發競態條件,造成數據不一致或者更嚴重的問題。 sync.Map是Go語言中內置的一種併發安全的map,但是他的實現和用法與普通的map完全不同,這篇文章將詳細介紹這些 ...
前言
在併發編程中,我們經常會遇到多個goroutine同時操作一個map的情況。如果在這種情況下直接使用普通的map,那麼就可能會引發競態條件,造成數據不一致或者更嚴重的問題。
sync.Map
是Go語言中內置的一種併發安全的map,但是他的實現和用法與普通的map完全不同,這篇文章將詳細介紹這些區別。
一、使用方法
創建sync.Map
非常簡單,只需要聲明即可:
var m sync.Map
使用Store
方法存儲鍵值對:
m.Store("hello", "world")
使用Load
方法獲取值:
value, ok := m.Load("hello") if ok { fmt.Println(value) // 輸出:world }
使用Delete
方法刪除鍵值對:
m.Delete("hello")
二、原理
sync.Map
的核心實現依賴於兩個主要的數據結構:一個只讀的read
欄位,以及一個可寫的dirty
欄位。
讀操作:
當我們進行讀取操作(Load
)時,首先會嘗試從read
欄位讀取數據,這個過程是完全無鎖的。
如果read
中沒有找到,那麼會嘗試加鎖後從dirty
中讀取。這個設計保證了在大部分讀多寫少的場景下,讀操作是無鎖的,大大提升了性能。
寫操作:
寫入操作(key不存在
)時,會直接在dirty
中進行寫入,並將read
的amended
標記為true
,表示read
欄位有待更新的數據。
然後再有新的讀取操作到來時,如果amended
為true並且miss數量超過dirty長度
,則會從dirty
中拷貝數據到read
,並清除amended
標記。
總結:
在這個設計中,讀操作在大部分情況下是無鎖的,而寫操作(key不存在時)則需要獲取dirty
的鎖,從而實現了對於讀多寫少場景的優化。
三、優點
sync.Map
在以下兩種情況下表現得特別好:
- 鍵值對的數量相對穩定,讀操作明顯多於寫操作的場景
- 多個goroutine併發讀取不重疊的鍵集的場景
這是因為sync.Map
的設計將讀取操作優化至極致,同時儘量減少在寫入新鍵值對時的鎖競爭。
四、缺點
然而,sync.Map
並不是銀彈,也有一些局限:
sync.Map
沒有像普通map那樣的直觀語法,必須使用特定的方法來操作鍵值對- 對於鍵值對數量快速增長、寫操作頻繁的場景,
sync.Map
的性能可能不如使用普通map加鎖的方式 - 讀操作無鎖情況下,可能會出現時間競態問題
五、實現
sync.Map
type Map struct { mu Mutex // read contains the portion of the map's contents that are safe for // concurrent access (with or without mu held). read atomic.Pointer[readOnly] // dirty contains the portion of the map's contents that require mu to be // held. To ensure that the dirty map can be promoted to the read map quickly, // it also includes all of the non-expunged entries in the read map. dirty map[any]*entry // misses counts the number of loads since the read map was last updated that // needed to lock mu to determine whether the key was present. misses int }
readonly
// readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { m map[any]*entry amended bool // true if the dirty map contains some key not in m. }
entry
// An entry is a slot in the map corresponding to a particular key. type entry struct { // p points to the interface{} value stored for the entry. p atomic.Pointer[any] }
expunged
// expunged is an arbitrary pointer that marks entries which have been deleted // from the dirty map. var expunged = new(any)
狀態機:
總結:
- 當key從read中刪除時,會先被標記為nil,不會立馬刪除key
- 當重新初始化dirty時(將read.m克隆到dirty),如果key的值為nil,會設置為expunged,並不在dirty中創建這個key。
- 如果key為expunged,LoadOrStore/Swap/CompareAndDelete/CompareAndSwap都會不執行寫操作並返回false。
Load
// 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 any) (value any, ok bool) { read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() // Avoid reporting a spurious miss if m.dirty got promoted while we were // blocked on m.mu. (If further loads of the same key will not miss, it's // not worth copying the dirty map for this key.) read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] // Regardless of whether the entry was present, record a miss: this key // will take the slow path until the dirty map is promoted to the read // map. m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() } func (m *Map) loadReadOnly() readOnly { if p := m.read.Load(); p != nil { return *p } return readOnly{} }
總結:
- 如果查詢的key在read中找到了,返回entry.load()
- 如果查詢的key在read中未找到,並且read和dirty一致,返回nil, false
- key未找到,並且read與dirty不一致
- 加鎖
- 重新查詢read,類似上面1、2流程,如果未找到並且read和dirty不一致則繼續
- 在dirty中查詢
- misses加一
- 如果misses數大於dirty長度,將dirty同步到read,重置dirty和misses
- 釋放鎖
- 返回結果
LoadOrStore
// LoadOrStore returns the existing value for the key if present. // Otherwise, it stores and returns the given value. // The loaded result is true if the value was loaded, false if stored. func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) { // Avoid locking if it's a clean hit. read := m.loadReadOnly() if e, ok := read.m[key]; ok { actual, loaded, ok := e.tryLoadOrStore(value) if ok { return actual, loaded } } m.mu.Lock() read = m.loadReadOnly() if e, ok := read.m[key]; ok { if e.unexpungeLocked() { m.dirty[key] = e } actual, loaded, _ = e.tryLoadOrStore(value) } else if e, ok := m.dirty[key]; ok { actual, loaded, _ = e.tryLoadOrStore(value) m.missLocked() } else { if !read.amended { // 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.Store(&readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) actual, loaded = value, false } m.mu.Unlock() return actual, loaded } // tryLoadOrStore atomically loads or stores a value if the entry is not // expunged. // // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and // returns with ok==false. func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) { p := e.p.Load() if p == expunged { return nil, false, false } if p != nil { return *p, true, true } // Copy the interface after the first load to make this method more amenable // to escape analysis: if we hit the "load" path or the entry is expunged, we // shouldn't bother heap-allocating. ic := i for { if e.p.CompareAndSwap(nil, &ic) { return i, false, true } p = e.p.Load() if p == expunged { return nil, false, false } if p != nil { return *p, true, true } } }
總結:
- 如果key在read中找到了,並且不為expunged
- 如果為nil,則CAS新的值,並返回value, false
- 如果不為nil,則返回*p, true
- 如果key在read中不存在,或者為expunged
- 加鎖
- 再次在read中查找,如果找到了
- 如果為expunged,結果為nil, false
- 如果為nil,則CAS新的值,結果為value, false
- 如果不為nil,結果為*p, true
- 如果在dirty中找到了,重覆2的邏輯判斷
- 在read和dirty中都沒有,則創建一個新的entry
- 釋放鎖
- 返回結果
Delete/LoadAndDelete
// Delete deletes the value for a key. func (m *Map) Delete(key any) { m.LoadAndDelete(key) }
// LoadAndDelete deletes the value for a key, returning the previous value if any. // The loaded result reports whether the key was present. func (m *Map) LoadAndDelete(key any) (value any, loaded bool) { read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] delete(m.dirty, key) // Regardless of whether the entry was present, record a miss: this key // will take the slow path until the dirty map is promoted to the read // map. m.missLocked() } m.mu.Unlock() } if ok { return e.delete() } return nil, false }
總結:
- 如果要刪除的key在read中存在,將它置為nil
- 如果要刪除的key在read中未找到,並且read和dirty一致,說明key不存在,返回nil, false
- key未找到,並且read和dirty不一致
- 加鎖
- 重新查詢read,類似上面1、2流程,如果key未找到,並且read和dirty不一致繼續
- 在dirty中查詢並刪除
- misses加一
- 如果misses數大於dirty長度,將dirty同步到read,重置dirty和misses
- 釋放鎖
- 如果key在dirty中也不存在,返回nil, false;反之,將它置為nil
Store/Swap
// Store sets the value for a key. func (m *Map) Store(key, value any) { _, _ = m.Swap(key, value) } // Swap swaps the value for a key and returns the previous value if any. // The loaded result reports whether the key was present. func (m *Map) Swap(key, value any) (previous any, loaded bool) { read := m.loadReadOnly() if e, ok := read.m[key]; ok { if v, ok := e.trySwap(&value); ok { if v == nil { return nil, false } return *v, true } } m.mu.Lock() read = m.loadReadOnly() if e, ok := read.m[key]; ok { if e.unexpungeLocked() { // The entry was previously expunged, which implies that there is a // non-nil dirty map and this entry is not in it. m.dirty[key] = e } if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else if e, ok := m.dirty[key]; ok { if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else { if !read.amended { // 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.Store(&readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } m.mu.Unlock() return previous, loaded } // trySwap swaps a value if the entry has not been expunged. // // If the entry is expunged, trySwap returns false and leaves the entry // unchanged. func (e *entry) trySwap(i *any) (*any, bool) { for { p := e.p.Load() if p == expunged { return nil, false } if e.p.CompareAndSwap(p, i) { return p, true } } }
總結:
- 如果key在read中找到了,並且不為expunged,則試圖CAS並返回結果
- key在read中未找到,或者為expunged
- 加鎖
- 在read中查詢
- 如果查到了,試圖unexpunge
- 如果需要unexpunge,會將entry置(CAS)為nil,併在dirty中插入key
- 執行entry的Swap
- 如果查到了,試圖unexpunge
- 在ditry中查詢
- 如果查到了,執行entry的Swap
- 都沒查到,則檢查dirty是否存在,初始化dirty,併在dirty增加新的entry
- 釋放鎖
- 返回結果
CompareAndSwap
// CompareAndSwap swaps the old and new values for key // if the value stored in the map is equal to old. // The old value must be of a comparable type. func (m *Map) CompareAndSwap(key, old, new any) bool { read := m.loadReadOnly() if e, ok := read.m[key]; ok { return e.tryCompareAndSwap(old, new) } else if !read.amended { return false // No existing value for key. } m.mu.Lock() defer m.mu.Unlock() read = m.loadReadOnly() swapped := false if e, ok := read.m[key]; ok { swapped = e.tryCompareAndSwap(old, new) } else if e, ok := m.dirty[key]; ok { swapped = e.tryCompareAndSwap(old, new) // We needed to lock mu in order to load the entry for key, // and the operation didn't change the set of keys in the map // (so it would be made more efficient by promoting the dirty // map to read-only). // Count it as a miss so that we will eventually switch to the // more efficient steady state. m.missLocked() } return swapped } // tryCompareAndSwap compare the entry with the given old value and swaps // it with a new value if the entry is equal to the old value, and the entry // has not been expunged. // // If the entry is expunged, tryCompareAndSwap returns false and leaves // the entry unchanged. func (e *entry) tryCompareAndSwap(old, new any) bool { p := e.p.Load() if p == nil || p == expunged || *p != old { return false } // Copy the interface after the first load to make this method more amenable // to escape analysis: if the comparison fails from the start, we shouldn't // bother heap-allocating an interface value to store. nc := new for { if e.p.CompareAndSwap(p, &nc) { return true } p = e.p.Load() if p == nil || p == expunged || *p != old { return false } } }
總結:
- 如果key在read中找到了
- 如果為nil/expunged/不等於old,則返回false
- 試圖進行entry的CAS,成功返回true,失敗繼續1、2流程
- 如果key在read中未找到,並且read與dirty沒有不一致,返回false
- 如果key在read中未找到,並且read與dirty不一致
- 加鎖
- 如果key在read中找到
- 如果仍然為nil/expunged/不等於old,結果為false
- 試圖進行entry的CAS,CAS成功,則結果為true;否則繼續1、2流程
- 如果key在dirty中找到
- 如果不等於old,結果為false
- 試圖進行entry的CAS,CAS成功,則結果為true;否則繼續1、2流程
- misses加一
- 如果misses數大於dirty長度,將dirty同步到read,重置dirty和misses
- 釋放鎖
- 返回結果
CompareAndDelete
// CompareAndDelete deletes the entry for key if its value is equal to old. // The old value must be of a comparable type. // // If there is no current value for key in the map, CompareAndDelete // returns false (even if the old value is the nil interface value). func (m *Map) CompareAndDelete(key, old any) (deleted bool) { read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] // Don't delete key from m.dirty: we still need to do the “compare” part // of the operation. The entry will eventually be expunged when the // dirty map is promoted to the read map. // // Regardless of whether the entry was present, record a miss: this key // will take the slow path until the dirty map is promoted to the read // map. m.missLocked() } m.mu.Unlock() } for ok { p := e.p.Load() if p == nil || p == expunged || *p != old { return false } if e.p.CompareAndSwap(p, nil) { return true } } return false }
總結:
- 如果key在read中找到了
- 如果為nil/expunged/不等於old,則返回false
- 試圖進行entry的CAS,成功返回true,失敗繼續1、2流程
- 如果key在read中未找到,並且read和dirty沒有不一致,返回false
- 如果key在read中未找到,並且read和dirty不一致
- 加鎖
- 從read中查找
- 如果找到key
- 為nil/expunged/不等於old,結果為false
- 試圖進行CAS,成功結果為true,失敗繼續嘗試
- 如果沒找到,並且沒有不一致,結果為false
- 如果找到key
- 如果read和dirty有不一致
- 從dirty中查找
- 如果找到key
- 不等於old,結果為false
- 試圖CAS,成功結果為true,失敗繼續嘗試
- 沒找到key,結果為false
- 釋放鎖
- 返回結果
結論
總的來說,sync.Map
是Go標準庫提供的一個非常有用的工具,它可以幫助我們簡化併發編程,並且在一些特定的場景下能提供良好的性能。
但在使用的時候,我們需要根據具體的應用場景和需求來選擇使用sync.Map
還是其他的併發原語。