本篇文章带大家学习Golang,深入理解下Golang中的sync.Map,希望对大家有所帮助!
mapmapsync.Mutexsync.Mapsync.Mutexsync.Mapmapsync.Mapsync.Mapsync.Mapmap 在并发下的问题
mapfatalmapaccess1mapkeymapfatalif 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.Mutexsync.Mutexvar 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.Mutexsync.Mutexsync.Mutex使用 sync.RWMutex 保证并发安全
sync.Mutexsyncsync.RWMutexsync.RWMutexvar 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)这样就不会报错了,而且性能也提高了,因为我们在读的时候,不需要等待锁。
说明:
RLockLockUnlocksync.RWMutexsync.Mutexsync.RWMutexKeys有了读写锁为什么还要有 sync.Map?
通过上面的内容,我们知道了,有下面两种方式可以保证并发安全:
sync.Mutexsync.RWMutexsync.Mutexsync.RWMutexsync.Mapsync.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,而且每次都不一样a2000020000var 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
BenchmarkMutexsync.Map 里面的原子操作
sync.Mapsync.RWMutexsync.MapLoadsync.Mapkeyreadkeysync.Mapkeysync.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.Mapsync.Mapmapsync.Mapsync.Mapsync.MapCRUDStoreStoreLoadRangeDeletevar 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 falsesync.Mapkeyvalueinterface{}keyvaluemapkeyvaluemap[any]anyRangefalsesync.Map 的使用场景
sync.Mapsync.MapThe 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 类型针对两种常见用例进行了优化:
keymapMutexRWMutexsync.Map总结
mapmapsync.Mutexsync.RWMutexsync.RWMutexsync.Mutexsync.Mapkeysync.Mutexsync.RWMutexsync.MapCRUDStoreStoreLoadRangeDeletesync.Mapsync.Mapkey更多编程相关知识,请访问:编程视频!!