atomic.addint64比mu.lock()快5倍因其编译为单条cpu指令(如lock xadd),纯用户态执行、无调度开销,延迟仅2–5 ns;而mu.lock()需系统调用和内核态切换,即使无争用也耗时50–100 ns,吞吐量相差3–10倍。

atomic.AddInt64 为什么比 mu.Lock() 快 5 倍
因为 atomic.AddInt64 编译成一条 CPU 指令(x86 是 lock xadd),纯用户态执行,无 goroutine 阻塞、无调度介入;而 mu.Lock() 即使无竞争也要走系统调用路径,触发 runtime_SemacquireMutex,耗时稳定在 50–100 ns,L1 cache 命中下 atomic.AddInt64 只要 2–5 ns。
实测吞吐差异明显:单核 10k goroutine 并发自增,atomic.AddInt64 达 20M ops/sec,mu.Lock() 通常掉到 2–5M ops/sec。
- 别信“加锁很快”的直觉——Mutex 的开销不在锁争用,而在每次调用都绕不开内核态切换
- atomic 不是“轻量锁”,它是硬件指令封装,不能替代锁的语义
- int32/int64/uintptr/指针/bool(via
Value)才支持原子操作;map、slice、struct 字段组合直接报错或行为未定义
benchmark 怎么写才不骗自己
把 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 只保证单个操作的原子性,不是锁的简化版。一旦逻辑跨出一步,就失效。
- 「读出来是 99,加 1 后写入」看似安全,但中间可能被其他 goroutine 改成 100,你再加就超限——这是典型的 ABA 问题,得用
atomic.CompareAndSwapInt64或退回Mutex - 「加 1 后立刻记录日志并回调」:日志和回调不在原子范围内,
atomic完全不保护它们 - 结构体里两个
int64字段挨得太近(如type Stats { Req int64; Err int64 }),高频更新会引发 false sharing,实际性能可能比Mutex还差——验证方式是go tool trace看到大量自旋但runtime.usleep很低
多字段更新或条件判断时该选谁
当操作涉及多个变量联动、读-改-写依赖判断、或结构体字段组合时,atomic 不仅不快,还容易引入竞态。
- 比如「余额 + 版本号」同时更新:用 CAS 循环实现复杂且易错,
Mutex直接包住整个临界区更清晰、更安全 - 「如果计数器 atomic.LoadInt64 和
atomic.AddInt64之间存在时间窗口,必须用CompareAndSwap或锁 - 读多写少场景(读:写 ≈ 100:1):
RWMutex.RLock()读开销约 5 ns,比atomic.LoadInt64(5 ns)相当,但写仍需RWMutex.Lock()(≈50 ns),此时选RWMutex更合理
真正难的不是“哪个快”,而是“哪段逻辑必须原子”——atomic 保护的是变量,Mutex 保护的是代码块。漏掉边界,再快也白搭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











