← 中文资源

简体中文版本

Go — fatal error:并发写 map

处理竞态检测器也可能漏掉的 fatal error: concurrent map writes,理解它为何不可恢复,并正确加锁或分片保护 map。

阅读英文原文

症状

  这种崩溃与普通 panic 最大的不同在于:无法恢复。fatal error: concurrent map writes 意味着运行时直接杀死进程,栈上任何 recover() 都会被无视。转储只点名一个正在写的 goroutine,但 bug 属于该 map 的所有未同步写者。还要注意检测器不对称:go test -race 可能反复通过而生产照崩——运行时检查与竞态检测器的采样方式不同,绿色的竞态结果不能为这个特定模式开脱。间歇性、只在特定并发级别复现,是采样式检查的天然属性。

诊断思路

  先读转储找被标记的写者及其栈(要修的代码所有者),但必须审计该 map 的全部写者;再 grep 该 map 的所有索引访问点,枚举必须统一同步策略的每个读写位置;最后即便利已先亮红也跑 -race,它常能在测试期就揪出冲突对,缩短审计。“实际只读”的 map 后来被缓存预热或惰性初始化路径写入,是渐进式引入的经典路径。

根因

  Go map 非并发安全。运行时插入的检查检测到并发访问即调用 fatal error 终止进程,recover 捕获不到它。检查是采样式的,违规在测试中漏网、在生产并发下现形。

修复与验证

  一把锁管住所有访问:sync.RWMutex(读多写少时读写锁摊薄成本)包住 Get/Set;键集不相交且以读为主的工作负载换 sync.Map 省样板;写密集并行改分片——按键哈希路由到 N 个各持锁的分片。无论选哪种,不变量必须是全量的:一条未同步的访问路径就能让 fatal 复活。验证时写一个按生产形态多 goroutine 轰炸该 map 的压力测试,带与不带 -race 各重复 20 遍以上克服采样偏差,双模式零崩溃才算达标;再把这个包的竞态套件永久留在 CI——运行时检查与检测器之间的采样差,正是这个 bug 能在绿灯测试后到达生产的原因。绝不要用 recover 或进程监护当同步的替代品,fatal error 根本不走那条路。

代码示例

fatal error: concurrent map writes

goroutine 87 [running]:
main.(*Cache).Set(...)
        /app/src/cache.go:44
type Cache struct {
    mu sync.RWMutex
    m  map[string]Entry
}

func (c *Cache) Get(k string) (Entry, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    e, ok := c.m[k]
    return e, ok
}

func (c *Cache) Set(k string, e Entry) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.m[k] = e
}
var cache sync.Map // key string, value Entry
cache.Store(k, e)
v, ok := cache.Load(k)