眾所周知關於Go的Map引用類型在多協程並發使用的時候不是協程安全的,使用Map進行並發修改時,如果低並發可能恰巧卡時間僥幸躲過。但高並發就沒那么僥幸了:fatal error: concurrent map read and map write
為什么不使用sync.Map
因此大部分人可能會尋求使用sync.Map來保證協程安全,讀寫不沖突。先照搬一下sync.Map的一般的使用和適用場景:
- 無須初始化,直接聲明即可。普通的Map是需要用make初始化的,sync.Map:
-
var scene sync.Map
-
- sync.Map 不能使用 map 的方式進行取值和設置等操作,而是使用 sync.Map 的方法進行調用,Store 表示新增和修改,Load 表示獲取,Delete 表示刪除。
-
// 存鍵值,也可以做修改操作 scene.Store("london", 100) // 根據鍵取值 fmt.Println(scene.Load("london")) // 根據鍵刪除鍵值對 scene.Delete("london")
- 而這三個函數的實現其實是通過鎖機制完成的,因此實現了多線程並發情況下的互斥操作Map
-
- sync.Map其設計的初衷也只是解決大並發讀寫的問題,且非常適合於大量讀少量寫的情況,但是當sync.Map存在大量新增和刪除時,會導致 dirty map 頻繁更新,甚至在 miss 過多導致 dirty 成為 nil,並進入重建。其實sync.Map是由一下兩個Map來實現的:(各Map的具體作用與Load,Store等方法的源碼可參考 此處 )
-
type Map struct { mu sync.Mutex read atomic.Value // readOnly dirty map[interface{}]*entry // 也就是說dirty map是由鎖來實現load和store的 misses int } type readOnly struct { m map[interface{}]*entry amended bool // true if the dirty map contains some key not in m. } // entry則是map中的value type entry struct { p unsafe.Pointer // *interface{} }
-
- 網上關於sync.Map與Map的性能比較:( 原文鏈接 )
sync.Map的性能高體現在讀操作遠多於寫操作的時候。 極端情況下,只有讀操作時,是普通map的性能的44.3倍。
反過來,如果是全寫,沒有讀,那么sync.Map還不如加普通map+mutex鎖呢。只有普通map性能的一半。
建議使用sync.Map時一定要考慮讀定比例。當寫操作只占總操作的<=1/10的時候,使用sync.Map性能會明顯高很多。
Map嵌套+Mutex實現方法
import "sync" type Map struct { m map[string]map[string]string // 自定義K-V的類型 sync.RWMutex } // 使用時初始化一個Map var theMap = &Map{ m: make(map[string]map[string]string) } // 讀出相應Key的子Map func (m *Map) LoadChildMap(key string) (value map[string]string, ok bool) { m.RLock() defer m.RUnlock() value, ok = m.m[key] return } // 讀出相應Key的子Map里相應childKey的val func (m *Map) LoadChildMapVal(key string, childKey string) (value string, ok bool) { m.RLock() defer m.RUnlock() valueMap, isOk := m.m[key] if !isOk { value, ok = "", false return } value, ok = valueMap[childKey] return } // 增加或修改相應Key的子Map func (m *Map) StoreChildMap(key string, value map[string]string) { m.Lock() defer m.Unlock() m.m[key] = value } // 增加或修改相應Key的子Map里相應childKey的val func (m *Map) StoreChildMapKV(key string, childKey string, value string) { m.Lock() defer m.Unlock() childMap, ok := m.m[key] if !ok { var newMap = make(map[string]string) newMap[childKey] = value m.m[key] = newMap return } childMap[childKey] = value } // 刪除相應Key的子Map func (m *Map) DeleteChildMap(key string) { m.Lock() defer m.Unlock() delete(m.m, key) } // 刪除相應Key的子Map里的相應childKey的val func (m *Map) DeleteChildMapVal(key string, childKey string) { m.Lock() defer m.Unlock() childMap, ok := m.m[key] if !ok { return } delete(childMap, childKey) }
- 需要注意的是,以上所有方法都是從頭到尾加鎖,若覺得粒度太大可以自己定義(甚至可以Map+Mutex整個結構嵌套,給每個子Map都設一把單獨的Mutex鎖,實現更小的粒度)。
- 在自行定義方法的時候,若僅僅在 value, ok = m.m[key] 的前后加鎖解鎖從而達到減小粒度,但請注意不要在解鎖后對value的任何值執行修改操作,因為Map的嵌套,value的類型是Map類型,是引用類型,對value進行操作實際上是對其指針進行操作,等效於繞過了鎖機制直接進行修改,極易發生 fatal error: concurrent map writes
- 解決辦法
- 將子Map值拷貝(需要深拷貝),待改好之后若需要更新到Map中,就再請求一個鎖
- 仍然遵守鎖機制,待所有修改完后才解鎖
- 當如果Map的value不是Map,Slice這類引用類型時,而是值傳遞的int,string等等,就無需考慮