在上一节 源码阅读(一):Golang map 我们讲到,map 不是并发安全的,本章我们就来了解下 map 为什么不是并发安全的?并发读写是会发生什么?如何保证 map 并发安全性问题?
为什么说 map 不是并发安全的
默认的 map
没有任何内置的并发控制机制,例如锁、信号量等。当多个 goroutine 同时读写 map 时,会处于竞争状态;例如以下几种情况:
- 当某个 goroutine 读/写某个
key
时,刚好有其他 goroutine 也进行读写,产生非预期值;
- 在读写 map 时,正处于重新分配内存空间或更新 bucket 哈希桶,导致数据结构异常;
从上节我们了解 map 源码的情况可知,默认提供一个无锁的 map,会显著降低实现的复杂度和提高性能,场景上更加通用化。
我们需要了解并发模型的一个概念: 同步顺序(synchronizes before)
1
在并发编程中内存模型,规定了某些操作(比如写操作 write)必须在其他操作(比如读操作 read)之前完成,就需要一些同步机制(如锁、信号量、通道等)来实现,以确保对共享资源的有序访问。
Why are map operations not defined to be atomic Go 官方博客这里解释了为什么 map 的操作不设计成原子性的。
- 在通常情况下使用 map 不需要对多个 goroutine 的并发访问进行并发安全保护,一般 map 都是某个更大的结构体或者计算的一部分,已经考虑了同步措施了;
- 如果要求所有的 map 的操作场景都需要使用锁(mutex),会比较影响性能,而只为了考虑少部分情况的安全性问题;
- map 的并发读取是安全的,当所有的 goroutine 都只是读取 map 的元素(查找或者是
for-range
遍历),在没有插入、删除等修改操作时并发访问都是安全的;
- 标准库也提供了
sync.Map
类型以解决并发读写的安全性问题,它使用在某些场景如静态缓存(static caches),但不适合作为内置 map 类型的通用替代品;
并发读写 map 时会发生什么
如何才能保证并发读写是安全的
方案一:Mutex 锁
这么实现并发问题解决了,但是性能上有比较大的影响;一种常见用法可以通过读写锁 sync.RWMutex
2 进一步优化:
方案二:sync.Map
让我们来改写下最上面的例子,使用 sync.Map
如何保证并发读写安全:
可以看到,最核心的两个方法就是 Load(key)
、Store(key, value)
,除此之外,sync.Map
还提供如下方法:
接下来,我们将借助 dlv 调试命令工具(如果想了解 dlv 的详细内容,可以参考这篇文章: 如何使用 dlv 进行 golang 代码调试)一步步看下 sync.Map
是如何保存元素的,具体代码如下:
前面我们提到了,当 m.misses
达到一定程度时,会把 dirty map
的数据迁移到 read map
,让我们来看看 m.missLocked()
具体是如何实现的: