如何在Golang中实现并发计数器_Golang sync/atomic原子操作示例

不能直接用普通 int 做并发计数,因为 counter++ 是非原子的“读-改-写”三步操作,会导致数据竞争、计数值偏小、偶发 panic 或负数;应使用 sync/atomic.AddInt64 等原子操作,操作对象须为对齐的 int64 指针。

Go 语言中用 sync/atomic 实现并发安全计数器,比加锁更轻量、更高效,但必须严格遵循原子操作的使用边界——不能用在需要复合逻辑(如“读-改-写”非单条原子指令)的场景。

为什么不能直接用普通 int 做并发计数

多个 goroutine 同时执行 counter++ 会导致数据竞争:该操作实际拆分为「读取值 → 加 1 → 写回」三步,中间可能被其他 goroutine 打断。Go 的 go run -race 会立刻报出 Data race 错误。

常见错误现象:

  • 终计数值远小于预期(例如启动 1000 个 goroutine 各自 +1,结果只有 300+)
  • 程序偶发 panic 或返回负数(因未对齐内存读写)
  • go build -race 检测到 Read at ... by goroutine N / Previous write at ... by goroutine M

sync/atomic.AddInt64 正确用法

这是最常用、最稳妥的并发计数方式。注意:操作对象必须是 int64 类型指针,且变量需对齐(全局变量或 struct 字段按 8 字节对齐即可)。

使用场景:

  • 统计请求数、错误数、活跃连接数等纯增量指标
  • 作为轻量信号量(如用 -1 表示关闭,用 atomic.CompareAndSwapInt64 切换状态)
  • 配合 atomic.LoadInt64 实现无锁读取

参数差异:

  • atomic.AddInt64(&counter, 1) 返回新值;atomic.AddInt64(&counter, -1) 可减
  • 不要传局部变量地址(如 func() { n := int64(0); atomic.AddInt64(&n, 1) }),逃逸分析可能引发隐患
  • 32 位系统上 int 非原子安全,务必显式用 int64 并调用对应函数
var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}

func get() int64 {
    return atomic.LoadInt64(&counter)
}

// 启动 100 个 goroutine 并发调用 increment()
for i := 0; i < 100; i++ {
    go increment()
}
time.Sleep(time.Millisecond)
fmt.Println(get()) // 稳定输出 100

CompareAndSwapInt64 用于条件更新

当计数逻辑含判断(如“仅当当前值

容易踩的坑:

  • 忘记循环:CAS 失败后不重试,逻辑就丢了
  • 用错旧值:第二个参数必须是「你认为当前应该有的值」,不是固定常量
  • 性能陷阱:高冲突下反复重试可能比 mutex 还慢,此时应考虑分片计数器(sharded counter)
func incrementIfLessThan(max int64) bool {
    for {
        old := atomic.LoadInt64(&counter)
        if old >= max {
            return false
        }
        if atomic.CompareAndSwapInt64(&counter, old, old+1) {
            return true
        }
        // CAS 失败,说明期间有别的 goroutine 改了值,重试
    }
}

真正要注意的是内存顺序和对齐——atomic 操作默认提供 sequential consistency,够用;但若嵌套在复杂结构体中,要确保字段偏移是 8 的倍数,否则在某些架构上 panic。这点常被忽略,尤其当计数器和其他字段混排时。