配置、路由表这类“读极多写极少”的数据,怎么读最便宜又不出错?这篇在 1 个和 8 个读者下比较 atomic.Pointer、atomic.Value、RWMutex、Mutex,验证为什么写者必须复制一份再替换指针,并列出 atomic.Value 的 panic 与分配细节。
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 次不一致。这就是“写时复制”。
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]:类型安全、不装箱。写者用“新建再替换”,读者读一次指针后只读不改。需要保护多个字段一起变的场景就别在原对象上改。