常说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并发安全的资料请关注代码网其它相关文章!
发表评论