map 不是并发安全的,但 Go 的运行时会尽力把这种错误变成一个立即的崩溃,而不是悄悄写坏数据。这篇用真实程序触发三种 fatal,验证 recover 接不住,量出检测的命中率,再读源码里检测用的那个标志,最后看加锁与 -race 的结果。
两个 goroutine 同时写(模式 ww):退出码 2;fatal error: concurrent map writes;输出里有 "recover 收到"?False 一个写、一个读(模式 rw):退出码 2;fatal error: concurrent map read and map write;输出里有 "recover 收到"?False 一个遍历、一个写(模式 iw):退出码 2;fatal error: concurrent map iteration and map write;输出里有 "recover 收到"?False (每个程序都用 defer + recover() 包住了 main:fatal error 不是 panic,defer 没有机会执行)
写与写、读与写、遍历与写都被检测。它们是 fatal error,等价于运行时认定程序已经不可恢复,所以 recover 接不住,进程以退出码 2 结束并打印所有 goroutine 的栈。生产环境里没法靠 recover 兜底这类错误。
2 个 goroutine 各写 10 万次:30 次里 fatal 30 次、正常结束 0 次;fatal 的信息分布:{'fatal error: concurrent map writes': 30}
2 个 goroutine 各只写 3 次:30 次里 fatal 2 次、正常结束 28 次;fatal 的信息分布:{'fatal error: concurrent map writes': 2}检测靠的是两个 goroutine 恰好同时处在 map 的写入窗口里。写 10 万次时窗口大量重叠,30 次全中;每个只写 3 次时,窗口几乎不重叠,28 次安静地结束。没触发 fatal 的程序仍然有数据竞争,-race 才是可靠的检查手段。
internal/runtime/maps/map.go:432: fatal("concurrent map read and map write")
internal/runtime/maps/map.go:451: fatal("concurrent map read and map write")
internal/runtime/maps/map.go:507: fatal("concurrent map writes")
internal/runtime/maps/map.go:533: fatal("concurrent map writes")
internal/runtime/maps/map.go:548: fatal("concurrent map writes")
internal/runtime/maps/map.go:677: fatal("concurrent map writes")
internal/runtime/maps/map.go:703: fatal("concurrent map writes")
internal/runtime/maps/map.go:762: fatal("concurrent map writes")
internal/runtime/maps/map.go:789: fatal("concurrent map writes")
internal/runtime/maps/map.go:808: fatal("concurrent map clone and map write")
internal/runtime/maps/table.go:882: fatal("concurrent map iteration and map write")
252: // writing is a flag that is toggled (XOR 1) while the map is being
253: // written. Normally it is set to 1 when writing, but if there are
256: writing uint8
431: if m.writing != 0 {
450: if m.writing != 0 {源码里 Map.writing 的注释写得很明确:写入期间它被“XOR 1”翻转;注释里还说明了为什么是翻转而不是置位:多个写者同时进入时,翻转能提高两边都检测到的概率。读路径与迭代器也检查这个标志。这是一个廉价的运行时检查,而不是锁:它不阻止竞争,只是在发现时崩溃。
mutex:10 次里正常结束 10 次;fatal 0 次 syncmap:10 次里正常结束 10 次;fatal 0 次 -race 编译的 ww 模式:退出码 2;输出里的关键行:WARNING: DATA RACE / Write at 0x… by goroutine 7: / Previous write at 0x… by goroutine 8: / fatal error: concurrent map writes -race 编译的 mutex 模式:退出码 0;输出:正常结束,len = 200000 / recover 收到: <nil>
用 sync.Mutex 包住 map,或者换成 sync.Map,都不再出错。-race 编译无保护的版本时,竞态检测器先报告 DATA RACE(并指出两个 goroutine 的写位置),运行时的 fatal 随后依然触发。读多写少的场景可以用 sync.RWMutex(见后面的 RWMutex 一篇),sync.Map 适用的场景见 sync.Map 一篇。