优先用 sync/atomic 实现计数器,因其比 sync.mutex 快10倍以上;仅复合操作需 mutex;atomic.addint64 等函数底层调用 cpu 原子指令,安全高效,但须确保变量在堆或全局,禁用栈局部变量和 float64/struct 直接原子操作。

直接用 sync/atomic,别碰 sync.Mutex —— 除非你要做复合操作(比如“加1后判断是否超限”),否则锁带来的开销是原子操作的 10 倍以上。
为什么不用 Mutex 实现简单计数器
多数人第一反应是封装一个带 Mutex 的结构体,但这是过重设计:
-
Mutex.Lock()在高竞争下会触发自旋 + 信号量等待,实测在 100+ goroutine 并发递增时,延迟从15ns暴涨到200ns+ -
sync.Mutex是为保护临界区设计的,而纯计数本身是无状态、幂等、可线性化的操作 - Go 的
atomic支持int64、uint64、uintptr和指针类型,覆盖绝大多数计数场景
atomic.AddInt64 是最常用且安全的选择
它底层调用 CPU 的 LOCK XADD 指令(x86)或 LDAXR/STLXR(ARM),保证单条指令的原子性,无需内存屏障干预(Go runtime 已隐式处理):
var counter int64
// 安全递增
atomic.AddInt64(&counter, 1)
// 安全读取(注意:非原子读可能看到中间态,必须用 Load)
value := atomic.LoadInt64(&counter)
// 安全比较并交换(CAS),用于条件更新
if atomic.CompareAndSwapInt64(&counter, 0, 100) {
// 只有原值为 0 时才设为 100
}
关键约束:counter 必须是全局变量或堆上变量;栈上局部变量地址不可靠,不能取地址传给 atomic 函数。
避免误用:不要对 float64 或 struct 做原子操作
sync/atomic 不支持浮点数或任意结构体的原子更新:
- 想原子更新
float64?必须转成uint64用atomic.LoadUint64+math.Float64bits/math.Float64frombits转换 - 想原子更新含多个字段的结构体?要么拆成独立原子变量,要么用
atomic.Value(仅适用于指针或接口类型,且写入成本高) - 常见错误:
atomic.AddInt64(&myStruct.count, 1)合法,但atomic.AddInt64(&myStruct, 1)编译不通过
需要线程本地缓存时:用 sync.Pool + atomic 协同
极端高频计数(如每秒千万级)下,单纯 atomic 仍可能因总线争用成为瓶颈。此时可引入分片计数 + 合并:
- 分配 N 个
int64变量(N 通常取 CPU 核心数),每个 goroutine 固定写其中一个(通过goroutine ID % N或runtime.LockOSThread绑定) - 主逻辑用
atomic更新对应分片,汇总时遍历所有分片求和 - 比全局原子操作降低争用,但增加读取复杂度;一般场景没必要,只有压测发现
atomic成为瓶颈时才启用
真正难的不是“怎么写快”,而是“怎么确认它没被其他 goroutine 干扰”——只要变量生命周期跨 goroutine、且未用 atomic 或 mutex 保护,就存在数据竞争,go run -race 会立刻报出。别依赖“好像没出错”。











