bigywater.tech
Go · 73

atomic.Value 与 atomic.Pointer:无锁读配置的写法

配置、路由表这类“读极多写极少”的数据,怎么读最便宜又不出错?这篇在 1 个和 8 个读者下比较 atomic.Pointer、atomic.Value、RWMutex、Mutex,验证为什么写者必须复制一份再替换指针,并列出 atomic.Value 的 panic 与分配细节。

0.1 vs 78.8 ns
8 个 goroutine 读配置:atomic.Pointer.Load 对 RWMutex.RLock
106250 次
原地覆盖共享 Config 时读者读到 A≠B 的次数,复制替换指针是 0 次
3 种 panic
atomic.Value 的 Store(nil)、类型不一致两种场景,原文已摘录

读很便宜

实测 · 本机执行输出
  G=1 atomic.Pointer.Load      0.4 ns/次
  G=1 atomic.Value.Load        0.9 ns/次
  G=1 RWMutex.RLock            7.8 ns/次
  G=1 Mutex.Lock               7.8 ns/次
  G=8 atomic.Pointer.Load      0.1 ns/次
  G=8 atomic.Value.Load        0.2 ns/次
  G=8 RWMutex.RLock           78.8 ns/次
  G=8 Mutex.Lock             117.8 ns/次

读者变多时锁的代价迅速上升(8 个读者:RWMutex 78.8 ns、Mutex 117.8 ns),而 atomic.Pointer.Load 只有 0.1 ns,atomic.Value.Load 0.2 ns——它们没有共享写入。这是极短循环里的测量,绝对值要打折扣,但相对关系很明显。

写者必须复制

实测 · 本机执行输出
  [原地整体覆盖同一个 Config] 普通编译:读 256575831 次,读到 A != B 的有 106250 次
  [原地整体覆盖同一个 Config] -race 编译:退出码 66,WARNING: DATA RACE 1 次
  [新建 Config 再 atomic.Pointer.Store] 普通编译:读 210907612 次,读到 A != B 的有 0 次
  [新建 Config 再 atomic.Pointer.Store] -race 编译:退出码 0,WARNING: DATA RACE 0 次

原地覆盖共享的 Config,读者拷贝时会读到半新半旧(106250 次 A≠B),-race 也报告竞争;写者新建一个完整对象再 Store 指针,读者永远拿到不再被修改的完整对象,0 次不一致。这就是“写时复制”。

atomic.Value 的坑

实测 · 本机执行输出
  v.Store(nil):sync/atomic: store of nil value into Value
  先 Store(int),再 Store(string):sync/atomic: store of inconsistently typed value into Value
  Store(*Config) 后 Store(Config):sync/atomic: store of inconsistently typed value into Value
  零值 Value.Load() = <nil>

Store(nil) 会 panic;第一次存什么类型,之后只能存同一个类型,否则 panic;零值 Load 返回 nil,类型断言前要判断。atomic.Pointer[T] 是泛型版本,类型在编译期固定,这几个运行时坑都没有(本文没有单独测它的零值)。

分配与复制

实测 · 本机执行输出
  atomic.Value.Store(*Config):每次 0 次分配
  atomic.Value.Store(Config 值,80 字节):每次 1 次分配
  atomic.Pointer.Store(*Config):每次 0 次分配
  atomic.Value.Load():每次 0 次分配;atomic.Pointer.Load():每次 0 次分配

往 atomic.Value 存指针不分配,存 80 字节的结构体值会分配一次(装箱);atomic.Pointer 存指针不分配。二者都带 noCopy 标记,go vet 会报复制。

怎么选

实测 · 本机执行输出
  go vet 输出:
    main.go:12:7: assignment copies lock value to b: lab.S contains sync/atomic.Pointer[int] contains sync/atomic.noCopy
    main.go:13:6: assignment copies lock value to _: lab.S contains sync/atomic.Pointer[int] contains sync/atomic.noCopy

新代码优先用 atomic.Pointer[T]:类型安全、不装箱。写者用“新建再替换”,读者读一次指针后只读不改。需要保护多个字段一起变的场景就别在原对象上改。

分步演示

atomic.Value 与 atomic.Pointer
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。读基准自己写:4000 万次读分给 G 个 goroutine,每格 3 次中位数,GOMAXPROCS=8,循环里只读 cfg.A 求和;一致性实验 6 个读者、写者 2 万次(每次约 5 µs),Config 的 A 与 B 隔了 6 个 int 防止被一条指令存下;-race 版本另外编译;panic 文字直接来自运行时;go vet 结果取自 Go 1.27.1 自带的 vet
    • 没有验证:atomic.Pointer 的零值行为、CompareAndSwap 的用法、Value 的内部实现(没有读源码)、写者频繁时的表现、arm64 之外平台的结果、读基准里编译器是否把循环优化得过于激进
    京ICP备2026067502号-1京公网安备11010502063487号