Go实现map并发安全的3种方式总结
实现map并发读写线程安全
1. 加锁
对整个map加上读写锁sync.RWMutex
//keyType为key的类型,valueType为value的类型 type RWMap struct { Map map[keyType]valueType sync.RWMutex } func NewRWMap(capacity int) *RWMap { if capacity < 0 { capacity = 0 } return &RWMap{ Map: make(map[keyType]valueType, capacity), } } //add or update func (m *RWMap) Set(key keyType, value valueType) { m.Lock() defer m.Unlock() m.Map[key] = value } //delete func (m *RWMap) Delete(key int) { m.Lock() defer m.Unlock() delete(m.Map, key) } //get func (m *RWMap) Get(key int) valueType { m.RLock() defer m.RUnlock() return m.Map[key] }
优点:解决了问题。
缺点:锁粒度大。
2. 分片加锁
一个操作会导致整个map被锁住,导致性能降低。所以提出了分片思想,将一个map分成几个片,按片加锁。
第三方包实现:github.com/orcaman/concurrent-map
github上用 map language:go 搜索:
3.4kstar
插曲:注意,如果你的goland ide 版本太老的话,github.com/orcaman/concurrent-map/v2 版本是用不了的:
所以我最后换成VSCode,发现就没这个问题了。(因为新版本的GoLand还得继续想法子破解)
源码New方法返回的map,看到key只支持string
// Creates a new concurrent map. func New[V any]() ConcurrentMap[string, V] { return create[string, V](fnv32) }
Example and usage
package main import ( "fmt" "time" cmap "github.com/orcaman/concurrent-map/v2" ) func main() { m := cmap.New[int]() for i := 0; i < 300; i++ { go func(i int) { m.Set(fmt.Sprintf("%v", i), i*2) //并发写 }(i) } time.Sleep(4 * time.Second) fmt.Println(len(m.Keys())) }
execute and output:
PS C:\GoWork\src\asset-manager\mytest> go run main.go 300
并发写没问题。
更多使用示例包里的concurrent_map_test.go里面提供了。
3. sync.Map
标准库中的 sync.Map是专为 append-only 场景设计的。
sync.Map在读多写少性能比较好,否则并发性能很差。
Go源码:
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use // by multiple goroutines without additional locking or coordination. // Loads, stores, and deletes run in amortized constant time. //=====自注释====== sync.Map 很像Go map[interface{}]interface{}。但sync.Map是线程安全的,能被多个协程在没有额外的锁或者协调的情况下并发使用。 Loads,stores,deletes操作都运行在分摊常数时间内。 amortized 平摊的(adj.)英 /əˈmɔːtaɪzd/ //=====自注释====== // // The Map type is specialized. Most code should use a plain Go map instead, // with separate locking or coordination, for better type safety and to make it // easier to maintain other invariants along with the map content. //=====自注释====== invariants (n.) 不变量(invariant的复数)/ɪnˈveriənts/ sync.Map 类型是为特殊情况专门设计的。 大多数代码都应该使用普通的Go map + 单独的锁或者协调 ,这种形式,来获得更好的类型安全 以及使得在维护映射内容的同时维护其他不变量更容易。 //=====自注释====== // // The Map type is optimized for two common use cases: (1) when the entry for a given // key is only ever written once but read many times, as in caches that only grow, // or (2) when multiple goroutines read, write, and overwrite entries for disjoint // sets of keys. In these two cases, use of a Map may significantly reduce lock // contention compared to a Go map paired with a separate Mutex or RWMutex. //=====自注释====== disjoint (adj.) 不连贯的,(两个集合)不相交的 /dɪsˈdʒɔɪnt/ sync.Map类型针对两个常见用例进行了优化: (1)对于一个给定的key,只会写一次,但是读很多次,就像在只增长的缓存中一样。 (2)当多个协程读,写,重写不相交的keys。 以上两种情况,相比于使用Go map + Mutex(或者RWMutex),使用sync.Map能显著减少锁竞争。 //=====自注释====== // // The zero Map is empty and ready for use. A Map must not be copied after first use. 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). // // The read field itself is always safe to load, but must only be stored with // mu held. // // Entries stored in read may be updated concurrently without mu, but updating // a previously-expunged entry requires that the entry be copied to the dirty // map and unexpunged with mu held. read atomic.Value // 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. //=====自注释====== expunged (adj.)/ɪkˈspʌndʒ/ 被擦去的,被删掉的 dirty map 包含map内容的部分,该部分要求持有mu锁。为了确保dirty map能快速提升到read map, 它还包括read map 中所有未删除的项。 //=====自注释====== // // Expunged entries are not stored in the dirty map. An expunged entry in the // clean map must be unexpunged and added to the dirty map before a new value // can be stored to it. // // If the dirty map is nil, the next write to the map will initialize it by // making a shallow copy of the clean map, omitting stale entries. 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. // // Once enough misses have occurred to cover the cost of copying the dirty // map, the dirty map will be promoted to the read map (in the unamended // state) and the next store to the map will make a new dirty copy. misses int }
read atomic.Value
sync/stomic包里都是go提供的原子操作。
sync.Map思想:就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,并最小粒度加锁,来减少锁对性能的影响。
总结
较常使用的是前两种:加读写锁和分片加锁。特定场景下sync.Map性能会有更优的表现(要满足那两个场景条件比较苛刻,实际很少用)。
最新评论