bigywater.tech
常见报错 · 42

Go fatal error: concurrent map writes:recover 抓不到,每个 goroutine 写 10 次时检测只发现 11/30,sync.Map 在写占 1% 时比 Mutex 快 11 倍

“fatal error: concurrent map writes” 是 Go 里最常见的线上崩溃之一。这篇在本机复现它:看 recover 能不能救、三种文字分别对应什么操作、运行时的检测会漏掉多少次、-race 补上多少,以及 Mutex、RWMutex、分片和 sync.Map 四种修法的吞吐。

11 / 30
每个 goroutine 只写 10 次时,运行时发现的次数;其余 19 次悄悄跑完
0 / 30
GOMAXPROCS=1 下写 10 万次也一次没发现
111.0
写占 1% 时 sync.Map 的吞吐(百万次/秒),sync.Mutex 只有 9.6

复现与 recover

实测 · 本机执行输出
  ww 10 万次(退出码 2,用时 0.60 s):
      fatal error: concurrent map writes
      goroutine 34 [running]:
      internal/runtime/maps.fatal({0x100610453?, 0x0?})
      	<GOROOT>/src/runtime/panic.go:1195 +0x20
      main.main.func1(0x0)
      	<tmp>/m/main.go:11 +0x74
      created by main.main in goroutine 1
      	<tmp>/m/main.go:11 +0x23c
  main 和 goroutine 里都 recover(退出码 2,用时 0.31 s):
      fatal error: concurrent map writes
      goroutine 19 [running]:
      internal/runtime/maps.fatal({0x102cef213?, 0x0?})
      	<GOROOT>/src/runtime/panic.go:1195 +0x20
      main.main.func2(0x0)
      …(共 8 行)

fatal error,进程必崩,recover 无效。

三种文字

实测 · 本机执行输出
  两个写:10 次运行结果 {'fatal error: concurrent map writes': 10}
  一个写一个读:10 次运行结果 {'fatal error: concurrent map read and map write': 10}
  一个遍历一个写:10 次运行结果 {'fatal error: concurrent map iteration and map write': 10}

writes、read and map write、iteration and map write,对应不同的访问组合。

检测不保证

实测 · 本机执行输出
  每个 goroutine 写 10 次,GOMAXPROCS=默认:被发现 11/30,悄悄跑完 19/30(其中 len(m) 不等于应有的 20 的有 0 次)
  每个 goroutine 写 100 次,GOMAXPROCS=默认:被发现 29/30,悄悄跑完 1/30(其中 len(m) 不等于应有的 200 的有 0 次)
  每个 goroutine 写 1000 次,GOMAXPROCS=默认:被发现 30/30,悄悄跑完 0/30(其中 len(m) 不等于应有的 2000 的有 0 次)
  每个 goroutine 写 10000 次,GOMAXPROCS=默认:被发现 30/30,悄悄跑完 0/30(其中 len(m) 不等于应有的 20000 的有 0 次)
  每个 goroutine 写 100000 次,GOMAXPROCS=默认:被发现 30/30,悄悄跑完 0/30(其中 len(m) 不等于应有的 200000 的有 0 次)
  每个 goroutine 写 100000 次,GOMAXPROCS=1:被发现 0/30,悄悄跑完 30/30(其中 len(m) 不等于应有的 200000 的有 0 次)

小数据量或单核时常漏,没崩溃不代表安全。

只读与 -race

实测 · 本机执行输出
  8 个 goroutine 只读 map,30 次运行:被报错 0/30
  -race,每个 goroutine 写 10 次:10 次里报 DATA RACE 9/10
  第一次报告的前几行(退出码 66):
      ==================
      WARNING: DATA RACE
      Write at 0x00c00018c120 by goroutine 7:
        runtime.mapaccess2_fast64()
            <GOROOT>/src/internal/runtime/maps/runtime_fast64.go:22 +0x2ac
        main.main.func1()
            <tmp>/mr/main.go:11 +0xb0
        main.main.gowrap1()
            <tmp>/mr/main.go:11 +0x38
  -race,每个 goroutine 写 100000 次:10 次里报 DATA RACE 10/10

只读并发安全;-race 报告率高得多但也不是 100%。

四种修法

实测 · 本机执行输出
  写占 1%:
    sync.Mutex         9.6
    sync.RWMutex      26.4
    sync.Map         111.0
    32 个分片+Mutex   38.1
  写占 10%:
    sync.Mutex         8.0
    sync.RWMutex      25.2
    sync.Map          82.8
    32 个分片+Mutex   34.9
  写占 50%:
    sync.Mutex         6.5
    sync.RWMutex      12.4
    sync.Map          28.7
    32 个分片+Mutex   27.6

Mutex、RWMutex、分片、sync.Map,读多写少时 sync.Map 最快,写多时与分片接近。

分步演示

并发 map
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64;报错文本都是真实复现时原样捕获的(命令行工具统一用 LC_ALL=C 取英文原文,路径换成 );所有服务都是本机上自己起的、只监听 127.0.0.1,不访问外网;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用 go1.27.3 darwin/arm64,10 个 CPU 核;每个程序是 go build 后直接运行的小 main.go;吞吐基准是 8 个 goroutine × 每个 20 万次操作 × 1024 个 key,5 轮取中位数,数字每次运行会有小幅抖动
    • 没有验证:Linux 上的行为(检测依赖运行时的写标志位,不依赖操作系统);key 很多或 value 很大时四种方案的相对快慢;sync.Map 在频繁新增不同 key 时的表现;Go 1.27 之前版本的检测概率;atomic.Value + 写时复制这种方案;为什么 GOMAXPROCS=1 时检测不到(推测是单个 P 上 goroutine 很少在 map 写的中途被抢占,没有查运行时源码)
    京ICP备2026067502号-1京公网安备11010502063487号