问题
今天收到同事发来的报错concurrent map read and map write
报错位置源码如下:
var data = make(map[string]int64, 100)
func GetValue(name string) int64 {
return data[name]
}
分析
看到这个报错,瞬间想到的就是同时写触发,但是报错的位置却是读。
模拟了同时读、同时读写的情况,都没有触发该报错。
下面是ai给出判断
这里关注到一个词潜在的panic
,这就很有意思了,这里的意思是读写混合并发不会必现路径。
深入猜测,那就可能是读时恰好map触发了扩容,果然,成功复现
成功复现案例
import (
"math/rand"
"sync"
"testing"
"time"
)
var m = make(map[string]int64, 100)
func TestReadMaps(t *testing.T) {
wg := sync.WaitGroup{}
wg.Add(40)
for i := 0; i < 20; i++ {
go func() {
for i := 0; i < 1000; i++ {
println(m["a"])
}
wg.Done()
}()
}
for i := 0; i < 20; i++ {
go func(x int) {
m[GenerateRandomString(x)] = rand.Int63n(99)
wg.Done()
}(i)
}
wg.Wait()
}
func GenerateRandomString(length int) string {
rand.New(rand.NewSource(time.Now().UnixNano()))
digits := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := ""
for i := 0; i < length; i++ {
index := rand.Intn(len(digits))
result += string(digits[index])
}
return result
}
虽然已经复现了,但是还是存在谜题。map初始容量是100,但是排查了实际情况,map最多写入74个key,不太应该触发扩容。
思考再三,应该是map扩容机制问题。下面是ai给出的原因
这里提到了哈希冲突过多,那确实是会有这种情况,key是string类型,74/100出现多次冲突是正常了
解决方案
- 使用sync.Map代替
- 添加读写锁sync.RWMutex确保读时不会触发扩容
- 如果已知map里的所有key,初始化更合适的map,确保数据不会触发扩容
我这里的情况刚好适合第3种解决方案,服务启动初始化阶段,就把所有的key都加载到map里面了,后续便不会再有扩容的情况发生