bigywater.tech
Go · 75

sync.Map:什么场景比 map 加锁快

sync.Map 什么时候真的比 map 加锁快?这篇用同一个一万键的负载,在 1 个和 8 个 goroutine、从只读到 50% 写的组合下量 sync.Map、RWMutex+map、Mutex+map,再看内存开销,并读源码确认它的实现。

5.2 vs 131.9 ns
8 个 goroutine 只读时,sync.Map 对 RWMutex+map
29.6 vs 16.8 ns
单 goroutine 只读时,sync.Map 反而更慢
3.2 倍
100 万个 int→int 时 sync.Map 占的堆是普通 map 的倍数

并发时赢很多,单线程反而慢

实测 · 本机执行输出
  单位 ns/次;顺序:sync.Map / RWMutex+map / Mutex+map
  只读(0% 写,共享键)                       G=1:    29.6 /    16.8 /    17.0
  只读(0% 写,共享键)                       G=8:     5.2 /   131.9 /   154.6
  99% 读 1% 写(共享键)                    G=1:    33.8 /    17.3 /    17.3
  99% 读 1% 写(共享键)                    G=8:     6.1 /    98.3 /   157.4
  50% 读 50% 写(共享键)                   G=1:    68.8 /    23.5 /    21.7
  50% 读 50% 写(共享键)                   G=8:    32.0 /    85.9 /   195.6
  50% 读 50% 写(每个 goroutine 只碰自己的键区间) G=1:    74.1 /    29.6 /    23.3
  50% 读 50% 写(每个 goroutine 只碰自己的键区间) G=8:    31.8 /    94.7 /   193.9

8 个 goroutine 读多写少,sync.Map 每次 5.2–6.1 ns,RWMutex+map 98–132 ns,Mutex+map 约 155 ns;写占到 50% 时 sync.Map 仍是 32 ns,明显优于另外两者(86–95 ns 和约 195 ns)。但只有 1 个 goroutine 时,sync.Map 29.6 ns,加锁的普通 map 只要 16.8–17.0 ns;50% 写时 68.8 对约 22 ns。没有并发竞争的时候不要用它。文档说“键互不相交”是它优化的第二种场景,我专门测了:50% 写、G=8 时 31.8 ns,和共享键的 32.0 ns 几乎一样,在这个测法下没有看到额外优势。

内存

实测 · 本机执行输出
  100 万个 int→int:普通 map 占堆 37.8 MB;sync.Map 占堆 121.6 MB(3.2 倍)

100 万个 int→int,sync.Map 占堆 121.6 MB,普通 map 37.8 MB。键和值都要装箱成 any,再加上字典树的节点(这个原因是我的推断,没有逐项量)。

实现已经换了

实测 · 本机执行输出
  Map is like a Go map[any]any but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes run in amortized constant time. 
  The Map type is specialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content. 
  The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may
  src/sync/map.go 里 Map 结构体的核心字段:m isync.HashTrieMap[any, any]
  internal/sync/hashtriemap.go:HashTrieMap is an implementation of a concurrent hash-trie. The implementation is designed around frequent loads, but offers decent performance for stores and deletes as well, especially if the map is larger. Its primary use-case is the unique package, but can be used elsewhere as well.

1.27.1 里 sync.Map 的核心字段是 isync.HashTrieMap[any, any],一个并发哈希字典树,注释说它围绕频繁的 Load 设计。我没有读它的具体算法,上面的快慢是实测。文档的态度仍然是:大多数代码应该用普通 map 加单独的锁。

分步演示

sync.Map
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。负载:预先放入 1 万个键,每次随机选键,按写比例决定读或写;800 万次操作平均分给 G 个 goroutine,墙钟时间 ÷ 800 万,每格 3 次取中位数,GOMAXPROCS=8;内存是 runtime.GC 两次后的 HeapAlloc 差;读过的源码与文档:go doc sync.Map,src/sync/map.go,src/internal/sync/hashtriemap.go 开头的注释
    • 没有验证:HashTrieMap 的具体算法与扩容、遍历(Range)在并发修改下的行为、键很少或很多时的曲线、非 int 键(字符串、结构体)的表现、Linux 上的结果、旧实现(Go 1.23 之前的 read/dirty)与现在的对比
    京ICP备2026067502号-1京公网安备11010502063487号