前言
有时候我们会遇到需要并发写map的时候,一般我们都会通过sync.Map
,map+sync.Mutex
或map+sync.RWMutex
来避免并发写map产生的异常。这时候会产生一个问题,如果不小心并发写map产生了异常,能否通过recover()来恢复?
注:使用的Go版本是1.18
一个并发写map的例子
package main
import (
"fmt"
)
func main() {
m := map[int]bool{}
go func() {
defer func() {
a := recover()
fmt.Println("1", a)
}()
for {
m[10] = true
}
}()
go func() {
defer func() {
a := recover()
fmt.Println("2", a)
}()
for {
m[10] = true
}
}()
select {}
}
这个例子是两个goroutine并发写一个map的一个key,在对于的函数里,使用了defer+recover()试图捕获并发写map产生的异常,主goroutine通过select{}进行阻塞等待。
我们运行这段代码,会产生以下错误信息:
fatal error: concurrent map writes
goroutine 7 [running]:
runtime.throw({0x2c7b35?, 0x2bc2c0?}) <-重点
C:/Program Files/Go/src/runtime/panic.go:992 +0x76 fp=0xc000051f68 sp=0xc000051f38 pc=0x253076
runtime.mapassign_fast64(0x2b65c0, 0xc000100480, 0xa) <-重点
C:/Program Files/Go/src/runtime/map_fast64.go:177 +0x2b4 fp=0xc000051fa0 sp=0xc000051f68 pc=0x22fbd4
main.main.func2()
D:/Github/him/_test/test.go:24 +0x50 fp=0xc000051fe0 sp=0xc000051fa0 pc=0x2ab770
runtime.goexit()
C:/Program Files/Go/src/runtime/asm_amd64.s:1571 +0x1 fp=0xc000051fe8 sp=0xc000051fe0 pc=0x27be81
created by main.main
D:/Github/him/_test/test.go:18 +0x7d
goroutine 1 [select (no cases)]:
main.main()
D:/Github/him/_test/test.go:27 +0x85
goroutine 6 [runnable]:
main.maiD:/Github/him/_test/test.go:15 +0x50
created by main.main
D:/Github/him/_test/test.go:9 +0x50
Process finished with the exit code 2
可以看到,是在调用runtime/map_fast64.go里面的mapassign_fast64()
的时候,在第177行代码调用了runtime.throw()
然后程序就结束了
我们先看看runtime/map_fast64.go的mapassign_fast64()函数:
func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
// 省略代码。。。
// 第一处
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 第二处
h.flags ^= hashWriting
// 省略代码。。。
again:
// 省略代码。。。
bucketloop:
// 省略代码。。。
done:
// 省略代码。。。
// 第三处
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// 第四处
h.flags &^= hashWriting
return elem
}
其中:
hashWriting = 4 // 表示有 goroutine 正在写map
我们上面的代码只留下和hashWriting
相关的代码,hashWriting
表示有 goroutine 正在写map。
我们按照上面分的4个位置:
第一处
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
在函数的入口处,先检查是否有goroutine正在写map,如果有说明并发写map,直接throw()
第二处
h.flags ^= hashWriting
这里用了异或操作
,而不是或操作
,是因为,如果有多个goroutine同时通过第一处
的判断,这里就会导致h.flags
对应hashWriting
的那一位变成0,这样在第三处
就会检查到
第三处
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
这时候,如果没有并发写map,按道理h.flags&hashWriting
结果应该是1,但是因为在并发情况下,可能多个写操作同时执行第二步
的h.flags ^= hashWriting
导致 h.flags
对应hashWriting
的那一位变成0,所以就会被检测到,然后throw()
第四处
h.flags &^= hashWriting
这里有一个比较特殊的&^
操作,它的规则如下:
第一个操作数 | 第二个操作数 | 结果 | 解释 |
---|---|---|---|
0 | 0 | 0 | 0&^0 |
0 | 1 | 0 | 0&^1 |
1 | 0 | 1 | 1&^0 |
1 | 1 | 0 | 1&^1 |
放到这个地方就是,0的位(除了hashWriting对应的位
)不会对左边操作数对应的位(h.flags
)产生影响,1的位(hashWriting对应的位
)会把左边操作数对应的位(h.flags
)清0
简单的说就是只把h.flags
里面hashWriting
对应的那位清0
简单来说
简单来说就是通过一个h.flags
去检测map是否被并发写,如果是的话就调用throw()
所以现在我们需要去看throw()
到底做了啥事情
runtime.throw()
func throw(s string) {
systemstack(func() {
print("fatal error: ", s, "\n")
})
gp := getg()
if gp.m.throwing == 0 {
gp.m.throwing = 1
}
fatalthrow()
*(*int)(nil) = 0 // not reached
}
- 首先它先打印了一句
fatal error: xxx
也就是上面看到输出的第一行 - 然后我们可以看到它调用了
fatalthrow()
,所以我们继续看fatalthrow()
是执行了什么
runtime.fatalthrow()
我们直接看这个函数的注释:
// fatalthrow implements an unrecoverable runtime throw. It freezes the
// system, prints stack traces starting from its caller, and terminates the
// process.
简单来说就是:
fatalthrow()
是无法被recover()
的- 它会冻结系统,打印堆栈,然后结束进程
fatalthrow()
里面最后会调用exit(2)
结束进程,也就是上面输出的最后一行Process finished with the exit code 2
所以我们现在知道了,如果map检测到被并发写入,会调用throw(),而throw()是无法被recover()
的,所以我们想试图通过recover()
来恢复是没用的
总结
- map会检测是否存在并发写
- 如果检测到并发写会调用
runtime.throw()
,无法被recover()
,直接GG - 如果要并发写map必须在业务层面上加锁(
sync.Mutex
或sync.RWMutext
)或使用sync.Map
等同步容器