map 在并发下的问题
go 里面提供了 map
这种类型让我们可以存储键值对数据,但是如果我们在并发的情况下使用 map
的话,就会发现它是不支持并发地进行读写的(会报错)。在这种情况下,我们可以使用 sync.Mutex
来保证并发安全,但是这样会导致我们在读写的时候,都需要加锁,这样就会导致性能的下降。
我们知道,Golang
原生的 map
是不支持并发的,如果你硬要用多个 goroutine
并发读写 map
,会得到 panic。
if h.flags&hashWriting != 0 {
fatal("concurrent map read and map write")
}
例如:
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]
}
}()
// 等待两个协程执行完毕 维持主goroutine
time.Sleep(time.Second)
执行这段程序,你会得到:fatal error: concurrent map read and map write
对此常见的解决方案有两种
- 使用 Go 1.9 之后引入的 sync.Map 实现,针对读多写少的场景做了优化;
- 原生 map 加上一把锁(Mutex 或 RWMutex)。
原生 map 如何识别并发
原理很简单:map 内置了标志位,语义是当前有一个goroutine正在写入,
hashWriting = 4 // a goroutine is writing to the map
使用使用 sync.Mutex/sync.RWMutex 保证并发安全
type SafeMap struct {
data map[interface{}]interface{}
sync.Mutex/sync.RWMutex
}
func (sa *SafeMap) Set(key interface{},value interface{}){
sa.Lock()
defer sa.UnLock()
sa.data[key]=value
}
func (sa *SafeMap) Get(key interface{})(value interface{}){
sa.Lock()
defer sa.UnLock()
value,ok := sa.data[key]
if !ok{
value=""
}
return
}
-
多个协程可以同时使用 RLock 而不需要等待,这是读锁。
-
只有一个协程可以使用 Lock,这是写锁,有写锁的时候,其他协程不能读也不能写。
-
写锁释放之后,其他协程才能获取到锁(读锁或者写锁)。
也就是说,使用 sync.RWMutex
的时候,读操作是可以并发执行的,但是写操作是互斥的。
有了读写锁为什么还要有 sync.Map?
- 使用
sync.Mutex
,但是这样的话,读写都是互斥的,性能不好。 - 使用
sync.RWMutex
,可以并发读,但是写的时候是互斥的,性能相对sync.Mutex
要好一些。
sync.Map
设计
sync.Map
在锁的基础上做了进一步优化,在一些场景下使用原子操作来保证并发安全,性能更好。因为原子操作直接有操作系统完成。
数据结构
// sync.Map
type Map struct {
// 互斥锁
mu sync.Mutex
// 只读 map,用于读操作
read atomic.Pointer[readOnly]
// dirty map,写入操作会先写入 dirty map
dirty map[any]*entry
// 记录需要从 dirty map 中读取 key 的次数。
// 也就是没有在 read map 中找到 key 的次数。
misses int
}
// readOnly 是一个只读的 map
type readOnly struct {
m map[any]*entry // dirty map 中的 key 的一份快照 与dirty 中的value是同一块内存
amended bool // 记录是否在 dirty map 中有部分 read map 中不存在的 key
}
// 实际存储值的结构体。
// p 有三种状态:nil, expunged, 正常状态。
type entry struct {
p atomic.Pointer[any]
}
mu
:它有一个 mu 互斥锁,用于保护 dirty map。read map
是一个只读的 map,但不是我们在其他地方说的只读。它的只读的含义是,它的 key 是不能增加或者删除的。但是value 是可以修改的
。dirty map
是一个可读写的 map,新增 key 的时候会写入 dirty map。dirty mapd的数据总是比read map的数据要新。misses
是一个 int 类型的变量,用于记录read map
中没有找到 key 的次数。当 misses 达到一定的值的时候,会将dirty map
中的 key 同步到read map
中。readOnly 和 dirty map
存储的信息都是entry
指针。好处- 1.减少空间浪费
- 2.修改值,可以改变指针指向,两处都可以生效。
dirty map 和 read map 之间的转换
写入新的 key
的时候,其实是写入到 dirty
中的,那什么时候会将 key
写入到 read
中呢? 准确来说,sync.Map 是不会往 read map 中写入 key 的,但是可以使用 dirty map 来覆盖 read map。
dirty map 转换为 read map
missess
的次数达到了len(dirty)
的时候。这意味着,很多次在read map
中都找不到key
,这种情况下是需要加锁才能再从dirty map
中查找的。- 使用
Range
遍历的时候,如果发现dirty map
中有些key
在read map
中没有,那么就会将dirty map
转换为read map
。然后遍历的时候就遍历一下 read map 就可以了。
read map 转换为 dirty map
dirty map
为nil
的情况下,需要往dirty map
中增加新的key
。- read map 转换为 dirty map 的时候,会将 read map 中正常的 key 复制到 dirty map 中。
但是这个操作完了之后,read map 中的那些被删除的 key 占用的空间是还没有被释放的。
那什么时候释放呢?那就是上面说的 dirty map 转换为 read map 的时候。
sync.Map 中 entry 的状态
在 sync.Map 中,read map 和 dirty map 中相同 key 的 entry 都指向了相同的内容(共享的)。
这样一来,我们就不需要维护两份相同的 value 了,这一方面减少了内存使用的同时,也可以保证同一个 key 的数据在 read 和 dirty 中看到都是一致的。
因为我们可以通过原子操作来保证对 entry 的修改是安全的(但是增加 key 依然是需要加锁的)。
entry 的状态有三种:
nil:被删除了,read map 和 dirty map 都有这个 key。
expunged:被删除了,但是 dirty map 中没有这个 key。
正常状态:可以被正常读取。
- key 被删除
- dirty map 为 nil 的时候,需要写入新的 key,read 中被删除的 key 状态会由 nil 修改为 expunged
- 被删除的 key,重新写入
- read 中被删除的 key(dirty map 中不存在的),在再次写入的时候会发生
注意:expunged 和正常状态之间不能直接转换,expunged 的 key 需要写入的话,需要先修改其状态为 nil。正常状态被删除之后先转换为 nil,然后在创建新的 map 的时候才会转换为正常状态。也就是 1->2 和 4->3 这两种转换)
不存在由正常状态转换为 expunged 或者由 expunged 转换为正常状态的情况。
sync.Map源码实现
先白话文说下大概逻辑。让下文看的更快。(大概只有是这样流程就好) 写:直写。 读:先读read,没有再读dirty。
查询
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 通过原子操作获取只读 map
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果read没有,并且dirty有新数据,那么去dirty中查找
if !ok && read.amended {
m.mu.Lock()
// 双重检查(原因是前文的if判断和加锁非原子的,害怕这中间发生故事)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 如果read中还是不存在,并且dirty中有新数据
if !ok && read.amended {
e, ok = m.dirty[key] // 从 dirty map 中获取
// m计数+1
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
// 将dirty置给read (原子操作,耗时很小)
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
为什么dirty置为nil?
猜测:
- 一方面是当read完全等于dirty的时候,读的话read没有就是没有了,即使穿透也是一样的结果,所以存的没啥用。
- 另一方是当存的时候,如果元素比较多,影响插入效率。
删
func (m *Map) Delete(key interface{}) {
// 读出read,断言为readOnly类型
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果read中没有,并且dirty中有新元素,那么就去dirty中去找。这里用到了amended,当read与dirty不同时为true,说明dirty中有read没有的数据。
if !ok && read.amended {
m.mu.Lock()
// 再检查一次,因为前文的判断和锁不是原子操作,防止期间发生了变化。
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
// 直接删除
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
// 如果read中存在该key,则将该value 赋值nil(采用标记的方式删除!)
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
// 再次再一把数据的指针
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
// 原子操作
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
存在于 read map
中,设置 entry 指针为 nil,但是不会删除 read map
中的 key;
只存在于 dirty map
中,则直接删除。这种情况下,会删除 dirty map
中的 key。
增(改)
func (m *Map) Store(key, value interface{}) {
// 如果m.read存在这个key,并且没有被标记删除,则尝试更新。
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 如果read不存在或者已经被标记删除
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok { // read 存在该key
// 如果entry被标记expunge,则表明dirty没有key,可添加入dirty,并更新entry。
if e.unexpungeLocked() {
// 加入dirty中,这儿是指针
m.dirty[key] = e
}
// 更新value值
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok { // dirty 存在该key,更新
e.storeLocked(&value)
} else { // read 和 dirty都没有
// 如果read与dirty相同,则触发一次dirty刷新(因为当read重置的时候,dirty已置为nil了)
if !read.amended {
// 将read中未删除的数据加入到dirty中
m.dirtyLocked()
// amended标记为read与dirty不相同,因为后面即将加入新数据。
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// 将read中未删除的数据加入到dirty中
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
// 遍历read。
for k, e := range read.m {
// 通过此次操作,dirty中的元素都是未被删除的,可见标记为expunged的元素不在dirty中!!!
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
// 判断entry是否被标记删除,并且将标记为nil的entry更新标记为expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
// 将已经删除标记为nil的数据标记为expunged
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
// 对entry尝试更新 (原子cas操作)
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
// read里 将标记为expunge的更新为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// 更新entry
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}