本篇文章带大家学习Golang,深入理解下Golang中的sync.Map,希望对大家有所帮助!
mapmapsync.Mutexsync.Mapsync.Mutexsync.Mapmapsync.Mapsync.Map
sync.Map
map 在并发下的问题
mapfatalmapaccess1mapkeymapfatal
if h.flags&hashWriting != 0 { fatal("concurrent map read and map write") }
map 并发读写异常的例子
下面是一个实际使用中的例子:
var m = make(map[int]int) // 往 map 写 key 的协程 go func() { // 往 map 写入数据 for i := 0; i < 10000; i++ { m[i] = i } }() // 从 map 读取 key 的协程 go func() { // 从 map 读取数据 for i := 10000; i > 0; i-- { _ = m[i] } }() // 等待两个协程执行完毕 time.Sleep(time.Second)
这会导致报错:
fatal error: concurrent map read and map write
mapmapmap
使用 sync.Mutex 保证并发安全
mapsync.Mutex
sync.Mutex
var m = make(map[int]int) // 互斥锁 var mu sync.Mutex // 写 map 的协程 go func() { for i := 0; i < 10000; i++ { mu.Lock() // 写 map,加互斥锁 m[i] = i mu.Unlock() } }() // 读 map 的协程序 go func() { for i := 10000; i > 0; i-- { mu.Lock() // 读 map,加互斥锁 _ = m[i] mu.Unlock() } }() time.Sleep(time.Second)
sync.Mutex
sync.Mutexsync.Mutex
使用 sync.RWMutex 保证并发安全
sync.Mutex
syncsync.RWMutex
sync.RWMutex
var m = make(map[int]int) // 读写锁(允许并发读,写的时候是互斥的) var mu sync.RWMutex // 写入 map 的协程 go func() { for i := 0; i < 10000; i++ { // 写入的时候需要加锁 mu.Lock() m[i] = i mu.Unlock() } }() // 读取 map 的协程 go func() { for i := 10000; i > 0; i-- { // 读取的时候需要加锁,但是这个锁是读锁 // 多个协程可以同时使用 RLock 而不需要等待 mu.RLock() _ = m[i] mu.RUnlock() } }() // 另外一个读取 map 的协程 go func() { for i := 20000; i > 10000; i-- { // 读取的时候需要加锁,但是这个锁是读锁 // 多个协程可以同时使用 RLock 而不需要等待 mu.RLock() _ = m[i] mu.RUnlock() } }() time.Sleep(time.Second)
这样就不会报错了,而且性能也提高了,因为我们在读的时候,不需要等待锁。
说明:
RLockLockUnlock
sync.RWMutexsync.Mutex
sync.RWMutexKeys
有了读写锁为什么还要有 sync.Map?
通过上面的内容,我们知道了,有下面两种方式可以保证并发安全:
sync.Mutexsync.RWMutexsync.Mutex
sync.RWMutexsync.Map
sync.Map
使用原子操作替代读锁
sync.RWMutex
举一个很常见的例子就是多个协程同时读取一个变量,然后对这个变量进行累加操作:
var a int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() wg.Wait() // a 期望结果应该是 20000才对。 fmt.Println(a) // 实际:17089,而且每次都不一样
a2000020000
var a atomic.Int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() wg.Wait() fmt.Println(a.Load()) // 20000
锁跟原子操作的性能差多少?
我们来看一下,使用锁和原子操作的性能差多少:
func BenchmarkMutexAdd(b *testing.B) { var a int32 var mu sync.Mutex for i := 0; i < b.N; i++ { mu.Lock() a++ mu.Unlock() } } func BenchmarkAtomicAdd(b *testing.B) { var a atomic.Int32 for i := 0; i < b.N; i++ { a.Add(1) } }
结果:
BenchmarkMutexAdd-12 100000000 10.07 ns/op BenchmarkAtomicAdd-12 205196968 5.847 ns/op
我们可以看到,使用原子操作的性能比使用锁的性能要好一些。
也许我们会觉得上面这个例子是写操作,那么读操作呢?我们来看一下:
func BenchmarkMutex(b *testing.B) { var mu sync.RWMutex for i := 0; i < b.N; i++ { mu.RLock() mu.RUnlock() } } func BenchmarkAtomic(b *testing.B) { var a atomic.Int32 for i := 0; i < b.N; i++ { _ = a.Load() } }
结果:
BenchmarkMutex-12 100000000 10.12 ns/op BenchmarkAtomic-12 1000000000 0.3133 ns/op
BenchmarkMutex
sync.Map 里面的原子操作
sync.Mapsync.RWMutexsync.MapLoadsync.Mapkeyreadkeysync.Mapkey
sync.MapLoad
// Load 方法从 sync.Map 里面读取数据。 func (m *Map) Load(key any) (value any, ok bool) { // 先从只读 map 里面读取数据。 // 这一步是不需要锁的,只有一个原子操作。 read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { // 如果没有找到,并且 dirty 里面有一些 read 中没有的 key,那么就需要从 dirty 里面读取数据。 // 这里才需要锁 m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] m.missLocked() } m.mu.Unlock() } // key 不存在 if !ok { return nil, false } // 使用原子操作读取 return e.Load() }
上面的代码我们可能还看不懂,但是没关系,这里我们只需要知道的是,从 sync.Map 读取数据的时候,会先做原子操作,如果没找到,再进行加锁操作,这样就减少了使用锁的频率了,自然也就可以获得更好的性能(但要注意的是并不是所有情况下都能获得更好的性能)。至于具体实现,在下一篇文章中会进行更加详细的分析。
也就是说,sync.Map 之所以更快,是因为相比 RWMutex,进一步减少了锁的使用,而这也就是 sync.Map 存在的原因了
sync.Map 的基本用法
sync.Mapsync.Mapsync.Map
sync.Mapmapsync.Mapsync.Mapsync.MapCRUD
StoreStoreLoadRangeDelete
var m sync.Map // 写入/修改 m.Store("foo", 1) // 读取 fmt.Println(m.Load("foo")) // 1 true // 遍历 m.Range(func(key, value interface{}) bool { fmt.Println(key, value) // foo 1 return true }) // 删除 m.Delete("foo") fmt.Println(m.Load("foo")) // nil false
sync.Mapkeyvalueinterface{}keyvaluemapkeyvaluemap[any]any
Rangefalse
sync.Map 的使用场景
sync.Mapsync.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.
翻译过来就是,Map 类型针对两种常见用例进行了优化:
key
mapMutexRWMutexsync.Map
总结
mapmapsync.Mutexsync.RWMutexsync.RWMutexsync.Mutexsync.Mapkeysync.Mutexsync.RWMutexsync.MapCRUDStoreStoreLoadRangeDeletesync.Mapsync.Mapkey
更多编程相关知识,请访问:编程视频!!