-
Go 语言原生
map并不是线程安全的,不支持并发读写(并发读写map 里的数据会被写乱,容易导致 panic ),对它进行并发读写操作的时候,需要加锁。 -
有以下两种方式可以实现
map的并发读写:
- 使用
sync.Mutex互斥锁。读和写的时候都使用互斥锁,性能相比sync.RWMutex会差一些。 - 使用
sync.RWMutex读写锁。读的锁是可以共享的,但是写锁是独占的。性能相比sync.Mutex会好一些。
1. sync.Map
sync.Map 是一种并发安全的 map,在 Go 1.9 引入。
sync.Map 里面会先进行原子操作来读取 key,如果读取不到的时候,才会需要加锁。所以性能相比 sync.Mutex 和 sync.RWMutex 会好一些。
sync.map是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
sync.map的零值是有效的,并且零值是一个空的 map。在第一次使用之后,不允许被拷贝。
一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错,
而使用 sync.Map 之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。
2. 使用方法(增删改查)
package main
import (
"fmt"
"sync"
)
func main() {
var s_map sync.Map
// 1. 写入
s_map.Store("name", "kyle")
s_map.Store("age", 18)
// 2. 读取
age, _ := s_map.Load("age")
fmt.Println(age.(int))
// 3. 遍历
s_map.Range(func(key, value any) bool {
name := key.(string)
age := value.(int)
fmt.Println(name, age)
return true
})
// 4. 删除(删除其中的一个 key,再读这个 key,得到的就是 nil)
s_map.Delete("name")
name, ok := s_map.Load("name")
fmt.Println(name, ok)
// 5. 读取时候如果不存在则写入,如果存在则只输出原值
s_map.LoadOrStore("name", "kyle")
name, _ = s_map.Load("name")
fmt.Println(name)
}
注意:在 sync.Map 中,key 和 value 都是 interface{} 类型的,也就是说,我们可以使用任意类型的 key 和 value。而不像 map,只能存在一种类型的 key 和 value。从这个角度来看,它的类型类似于 map[any]any。
另外一个需要注意的是,Range 方法的参数是一个函数,这个函数如果返回 false,那么遍历就会停止。
sync.Map 适用于:
- 当给定
key的条目只写入一次但读取多次时,如在只会增长的缓存中。(读多写少) - 当多个 goroutine 读取、写入和覆盖不相交的键集的条目。(不同 goroutine 操作不同的 key)
对于写多的场景,会导致 read map 缓存失效,需要加锁,导致冲突变多;而且由于未命中 read map 次数过多,导致 dirty map 提升为 read map,这是一个 O(N) 的操作,会进一步降低性能。