直接用int在goroutine里计数会出错,因为i++是非原子的read-modify-write操作,多goroutine并发执行时可能读到相同旧值并写回相同新值,导致结果丢失;必须用sync/atomic(如AddInt64)或sync.Mutex保证原子性。

如何在Golang中测试并发数据访问_Golang sync/atomic与Benchmark方法  第1张

为什么直接用 int 在 goroutine 里计数会出错

因为 int 的读写不是原子操作。多个 goroutine 同时执行 i++(即 read-modify-write)时,可能读到同一个旧值、各自加 1、再写回,导致最终结果比预期少。这不是“偶尔出错”,而是只要并发足够高、调度足够乱,就必然发生。

典型错误现象:

func TestCounterRace(t *testing.T) {
    var i int
    var wg sync.WaitGroup
    for range [1000]int{} {
        wg.Add(1)
        go func() {
            defer wg.Done()
            i++ // 这里有数据竞争
        }()
    }
    wg.Wait()
    if i != 1000 {
        t.Errorf("expected 1000, got %d", i) // 几乎必 fail
    }
}

  • 运行 go test -race 会立刻报出 Data race on variable i
  • 即使没开 race detector,结果也常是 992、997 等非 1000 值
  • sync.Mutex 能解决,但有锁开销;sync/atomic 更轻量,适合简单整型操作

sync/atomic 支持哪些类型和操作

sync/atomic 不支持任意类型,只对底层可原子操作的整型和指针提供封装:基本是 int32int64uint32uint64uintptr*unsafe.Pointer。注意:int 在 32 位系统上是 int32,64 位上是 int64 —— 但 atomic 不提供 int 版本函数,必须显式选 int32int64

  • atomic.AddInt64(&i, 1):返回新值,线程安全自增
  • atomic.LoadInt64(&i):安全读取当前值
  • atomic.StoreInt64(&i, 42):安全写入
  • atomic.CompareAndSwapInt64(&i, old, new):CAS,成功返回 true
  • 没有 atomic.IncInt64,只有 AddInt64(x, 1)

别用 int 变量配 atomic.AddInt64 —— 类型不匹配会编译失败:

var i int
atomic.AddInt64(&i, 1) // ❌ compile error: cannot use &i (type *int) as type *int64

怎么写一个靠谱的 Benchmark 测原子操作开销

基准测试要避免被编译器优化掉,也要控制变量。比如测 atomic.AddInt64 vs mutex vs 普通 ++(后者仅作对比,实际不能用),关键点是:所有操作必须作用在可逃逸的变量上,且结果要被使用(如赋给 b.ReportMetric 或全局变量),否则会被优化为无操作。

立即学习“go语言免费学习笔记(深入)”;

  • b.N 控制循环次数,不是硬写 1000000
  • 避免在循环内创建新 goroutine —— Benchmark 是单 goroutine 下的吞吐测算,不是压测并发行为
  • 想测并发场景下的性能?得用 go test -benchmem -benchtime=3s 并手动启 goroutine + sync.WaitGroup,但要注意结果解释:它测的是「N 个 goroutine 争抢一个原子变量」的吞吐,不是单次操作延迟

一个干净的单 goroutine 基准示例:

func BenchmarkAtomicAdd(b *testing.B) {
    var i int64
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        atomic.AddInt64(&i, 1)
    }
    // 强制使用 i,防止优化
    blackhole = i
}

var blackhole int64

atomic 在真实服务中容易被忽略的边界

很多人以为用了 atomic 就万事大吉,但几个关键限制常被忽视:

  • atomic 只保证单个操作原子性,不保证多字段间的一致性。比如你有两个 int64 字段 countsum,分别用 atomic 更新,但业务要求「每次 count+1 必须伴随 sum+=x」——这时仍需锁或更高级结构(如 atomic.Value 存整个 struct 指针)
  • atomic.Value 只支持 Store/Load,且存的值必须是相同类型;存 *MyStruct 后不能再存 *bytes.Buffer
  • 32 位 ARM 上,atomic.LoadUint64 等 64 位操作需要硬件支持,老设备可能 panic;生产环境建议统一用 int64 并确认目标平台支持
  • 没有 atomic.MinInt64atomic.MaxUint32 —— 这类操作得靠 CompareAndSwap 循环实现,写错容易死循环

真正复杂的共享状态,atomic 往往只是拼图一角。它快,但不够“聪明”。