详解Go语言如何解决map并发安全问题
作者:shark_chili
常说go语言是一门并发友好的语言,对于并发操作总会在编译期完成安全检查,所以这篇文章我们就来聊聊go语言是如何解决map这个数据结构的线程安全问题。
详解map中的并发安全问题
问题复现
我们通过字面量的方式创建一个map集合,然后开启两个协程,其中协程1负责写,协程2负责读:
func main() { //创建map m := make(map[int]string) //声明一个长度为2的倒计时门闩 var wg sync.WaitGroup wg.Add(2) //协程1写 go func() { for true { m[0] = "xiaoming" } wg.Done() }() //协程2读 go func() { for true { _ = m[0] } wg.Done() }() wg.Wait() fmt.Println("结束") }
在完成编译后尝试运行
fatal error: concurrent map read and map write
并发操作失败的原因
我们直接假设一个场景,协程并发场景下当前的map
处于扩容状态,假设我们的协程1
修改了key-111
对应的元素触发渐进式驱逐操作,使得key-111
移动到新桶上,结果协程2
紧随其后尝试读取key-111
对应的元素,结果得到nil
,由此引发了协程安全问题:
上锁解决并发安全问题
和Java
一样,go语言也有自己的锁sync.Mutex
,我们在协程进行map操作前后进行上锁和释放的锁的操作,确保单位时间内只有一个协程在操作map
,从而实现协程安全,因为这种锁是排他锁,这使得协程的并发特性得不到发挥:
var mu sync.Mutex func main() { //创建map m := make(map[int]string) var wg sync.WaitGroup wg.Add(2) //协程1上锁后写 go func() { for true { mu.Lock() m[0] = "xiaoming" mu.Unlock() } wg.Done() }() //协程2上锁后读 go func() { for true { mu.Lock() _ = m[0] mu.Unlock() } wg.Done() }() wg.Wait() fmt.Println("结束") }
使用自带的sync.map进行并发读写
好在go语言为我们提供的现成的"轮子"
,即sync.Map
,我们直接通过其内置函数store
和load
即可实现并发读写还能保证协程安全:
func main() { //创建sync.Map var m sync.Map var wg sync.WaitGroup wg.Add(2) //协程1并发写 go func() { for true { m.Store(1, "xiaoming") } wg.Done() }() //协程2并发读 go func() { for true { m.Load(1) } wg.Done() }() wg.Wait() fmt.Println("结束") }
详解sync.map并发操作流程
常规sync.map并发读或写
sync.map
会有一个read
和dirty
指针,指向不同的key数组
,但是这些key
对应的value
指针都是一样的,这意味着这个map
不同桶的相同key
共享同一套value
。
进行并发读取或者写的时候,首先拿到一个原子类型的read
指针,通过CAS尝试修改元素值,如果成功则直接返回,就如下图所示,我们的协程通过CAS
完成原子指针数值读取之后,直接操作read
指针所指向的map
元素,通过key
定位到value
完成修改后直接返回。
sync.map修改或追加
接下来再说说另一种情况,假设我们追加一个元素key-24
,通过read
指针进行读取发现找不到,这就意味当前元素不存在或者在dirty
指针指向的map
下,所以我们会先上重量级锁,然后再上一次read
锁。 分别到read
和dirty
指针上查询对应key,进行如下三部曲:
- 如果在
read
发现则修改。 - 如果在
dirty
下发现则修改。 - 都没发现则说明要追加了,则将
amended
设置为true
说明当前map
脏了,尝试将元素追加到dirty
指针管理的map
下。
这里需要补充一句,通过amended
可知当前map是否处于脏写状态,如果这个标志为true,后续每次读写未命中都会对misses
进行自增操作,一旦未命中数达到dirty
数组的长度(大抵是想表达所有未命中的都在dirty
数组上)阈值就会进行一次dirty提升,将dirty的key
提升为read
指针指向的数组,确保提升后续并发读写的命中率:
sync.map并发删除
并发删除也和上述并发读写差不多,都是先通过read指针尝试是否成功,若不成功则锁主mutex到dirty进行删除,所以这里就不多赘述了。
sync.map源码解析
sync.map内存结构
通过上文我们了解了sync.map的基本操作,这里我们再回过头看看sync.map的数据结构,即重量级锁mu Mutex,
type Map struct { //重量级锁 mu Mutex //read指针,指向一个不可变的key数组 read atomic.Pointer[readOnly] //dirty 指针指向可以进行追加操作的key数组 dirty map[any]*entry //当前map读写未命中次数 misses int }
sync.Map并发写源码
并发写底层本质是调用Swap
进行追加或者修改:
func (m *Map) Store(key, value any) { _, _ = m.Swap(key, value) }
步入swap底层即可看到上文图解的操作,这里我们给出核心源码,读者可自行参阅:
func (m *Map) Swap(key, value any) (previous any, loaded bool) { //上read尝试修改 read := m.loadReadOnly() if e, ok := read.m[key]; ok { if v, ok := e.trySwap(&value); ok { if v == nil { return nil, false } return *v, true } } //上重量级锁和read原子指针加载进行修改 m.mu.Lock() read = m.loadReadOnly() if e, ok := read.m[key]; ok { if e.unexpungeLocked() { m.dirty[key] = e } if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else if e, ok := m.dirty[key]; ok { //如果在dirty数组发现则上swap锁进行修改 if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else {//上述情况都不符合则将amended 标记为true后进行追加 if !read.amended { m.dirtyLocked() m.read.Store(&readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } //解锁返回 m.mu.Unlock() return previous, loaded }
sync.Map读取
对应的读取源码即加载read原子变量后尝试到read指针下读取,若读取不到则增加未命中数到dirty指针下读取:
func (m *Map) Load(key any) (value any, ok bool) { //加载读原子变量 read := m.loadReadOnly() //尝试在read指针下读取 e, ok := read.m[key] //没读取到上mutex锁到dirty下读取,若发现则更新未命中数后返回结果 if !ok && read.amended { 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() } if !ok { return nil, false } return e.load() }
sync.Map删除
删除步骤也和前面几种操作差不多,这里就不多赘述了,读者可参考笔者核心注释了解流程:
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) { //上读锁定位元素 read := m.loadReadOnly() e, ok := read.m[key] //为命中则上重量级锁到read和dirty下再次查找,找到了则删除,若是在dirty下找到还需要额外更新一下未命中数 if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] delete(m.dirty, key) //自增一次未命中数 m.missLocked() } m.mu.Unlock() } if ok { return e.delete() } return nil, false } // Delete deletes the value for a key. func (m *Map) Delete(key any) { m.LoadAndDelete(key) }
以上就是详解Go语言如何解决map并发安全问题的详细内容,更多关于Go解决map并发安全的资料请关注脚本之家其它相关文章!