atomic.addint64通常更快,因其编译为单条cpu原子指令(如lock xadd),纯用户态执行、无调度开销;而sync.mutex需系统调用、可能触发goroutine阻塞和调度,延迟高数十倍。

绝大多数计数器场景下,atomic.AddInt64 比 sync.Mutex 快 3–10 倍,但这个结论只在「纯自增/自减 + 独立字段」条件下成立;一旦涉及条件判断、多字段联动或结构体嵌套,atomic 不仅不快,还可能引入数据竞争。
为什么 atomic.AddInt64 在计数器上通常更快
atomic.AddInt64 直接编译为一条 CPU 指令(x86 是 lock xadd,ARM 是 stlxr),全程用户态执行,无 goroutine 阻塞、无调度介入、无内存分配。而 mu.Lock() 触发系统调用,争用高时会进入 runtime_SemacquireMutex,pprof 里常看到大量时间卡在 semacquire 或 mcall 上。
- 实测典型吞吐:单核 10k goroutine 并发自增,
atomic可达 20M ops/sec,Mutex通常掉到 2–5M ops/sec - 延迟差异明显:
atomic.AddInt64平均延迟约 2–5 ns(L1 cache 命中),mu.Lock()即使无争用也常在 50–100 ns - 注意:这个优势只对
int64/int32这类标量有效;对map、[]byte、结构体字段组合,atomic根本不能用
基准测试怎么写才不算白测
把 atomic.AddInt64 和 mu.Lock() 塞进同一个 Benchmark 函数里跑,结果基本不可信——变量复用、缓存残留、未重置初始值,都会污染数据。
- 必须拆成两个独立函数:
BenchmarkAtomicAdd和BenchmarkMutexAdd - 每个函数用独立变量:
var atomicCounter int64和var muCounter int64,绝不共用 -
b.ResetTimer()前必须重置:atomic.StoreInt64(&atomicCounter, 0)和muCounter = 0 - 循环体内只留核心操作:
atomic.AddInt64(&atomicCounter, 1)或mu.Lock(); muCounter++; mu.Unlock() - 禁用副作用:不调
fmt.Println、不 new slice、不触发 GC
哪些计数器场景 atomic 会翻车
atomic 只保证单个操作的原子性,它不是锁的轻量替代品,而是硬件级的“单指令开关”。一旦逻辑跨出一步,就失效。
- 「如果计数器 atomic.LoadInt64 读出来是 99,另一 goroutine 抢先加到 100,你再
atomic.AddInt64就超了——这是典型的 ABA 问题,得用atomic.CompareAndSwapInt64或退回到Mutex - 「加 1 后立刻记录日志并回调」:日志和回调不在原子范围内,
atomic完全不保护它们 - 结构体里两个
int64字段挨得太近:比如type Stats struct { Req int64; Err int64 },高频更新会引发 false sharing,实际性能可能比Mutex还差——验证方式是go tool trace看到大量自旋但runtime.usleep很低 - 写操作占比 >15% 且 goroutine 数 >> CPU 核数:
atomic持续自旋抢总线,Mutex反而因休眠更稳
struct 里用 atomic 字段最容易踩的坑
Go 不保证 struct 字段自动对齐,int64 若没落在 8 字节边界上,在 ARM 或 32 位系统会 panic:"panic: unaligned 64-bit atomic operation"。
- 最简解法:用 Go 1.21+ 的
atomic.Int64类型,它内部已做 cache-line 对齐,且自带方法封装,比裸指针安全得多 - 手动对齐:在字段前后加填充,例如
_ [12]byte(凑够 64 字节缓存行) - 别对局部变量取地址传给 atomic:
var c int64; go func(){ atomic.AddInt64(&c, 1) }()是数据竞争,必须是包级变量或堆分配(new(int64)) - 读取时直接
atomic.LoadInt64(&counter)即可,无需加锁;但别在 HTTP handler 里用fmt.Sprintf("%d", ...)拼响应——fmt开销远大于 atomic 本身
真正难的不是选 atomic 还是 Mutex,而是识别「这个操作到底是不是原子语义能覆盖的」。只要需求里出现“如果…就…”、“读完马上改”、“多个字段要一起变”,就该立刻收手,老老实实用锁——这不是性能妥协,是语义刚性使然。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











