文章目录

Go 原子操作:sync/atomic 包的使用

发布于 2026-04-04 00:40:49 · 浏览 2 次 · 评论 0 条

Go 原子操作:sync/atomic 包的使用

在并发编程中,多个 goroutine 同时读写同一个变量时,如果不加保护,会导致数据竞争(data race),产生不可预测的结果。Go 提供了 sync/atomic 包,用于实现对基本类型(如整数、指针)的无锁原子操作。这些操作由 CPU 指令直接保证,比使用互斥锁(sync.Mutex)更轻量、更高效。


何时使用 atomic?

优先考虑 sync/atomic 的场景

  • 只需要对单个变量进行简单操作(如自增、比较并交换)。
  • 性能敏感,希望避免锁带来的开销。
  • 不涉及复杂逻辑(如多个变量需同时更新)。

不要使用 atomic 的情况

  • 需要操作结构体字段(除非整个结构体用指针原子替换)。
  • 多个变量之间存在一致性要求(此时应使用 sync.Mutexsync.RWMutex)。

基础整数原子操作

Go 的 atomic 包为有符号和无符号整数提供了统一的操作接口。最常用的是 int64uint64 类型。

1. 加载 当前值

调用 atomic.LoadInt64 安全读取一个 int64 变量:

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var counter int64 = 100
    value := atomic.LoadInt64(&counter)
    fmt.Println(value) // 输出: 100
}

注意:必须传入变量的地址(&counter),因为函数内部通过指针直接读取内存。

2. 存储 新值

调用 atomic.StoreInt64 安全写入一个新值:

var counter int64
atomic.StoreInt64(&counter, 42)
fmt.Println(atomic.LoadInt64(&counter)) // 输出: 42

3. 自增/自减

调用 atomic.AddInt64 实现原子加法(支持负数,即减法):

var counter int64
atomic.AddInt64(&counter, 5)   // +5
atomic.AddInt64(&counter, -2)  // -2
fmt.Println(atomic.LoadInt64(&counter)) // 输出: 3

所有 AddXXX 函数都返回操作后的新值,可直接使用。

4. 比较并交换(CAS)

调用 atomic.CompareAndSwapInt64 实现“仅当当前值等于旧值时才更新”:

var counter int64 = 10
success := atomic.CompareAndSwapInt64(&counter, 10, 20)
fmt.Println(success, atomic.LoadInt64(&counter)) // true, 20

// 再次尝试:期望值是 10,但实际是 20,失败
success = atomic.CompareAndSwapInt64(&counter, 10, 30)
fmt.Println(success, atomic.LoadInt64(&counter)) // false, 20

CAS 是实现无锁数据结构(如队列、栈)的核心原语。


指针的原子操作

除了整数,atomic 还支持对指针的原子加载、存储和 CAS。

原子替换结构体指针

假设有一个配置对象,需要在运行时安全地热更新:

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

type Config struct {
    Host string
    Port int
}

func main() {
    var current atomic.Value // 使用 atomic.Value 更通用(见下文)
    // 但若坚持用指针:
    var cfgPtr unsafe.Pointer

    // 初始配置
    initial := &Config{Host: "localhost", Port: 8080}
    atomic.StorePointer(&cfgPtr, unsafe.Pointer(initial))

    // 读取配置
    readCfg := (*Config)(atomic.LoadPointer(&cfgPtr))
    fmt.Println(readCfg.Host) // localhost

    // 更新配置
    newCfg := &Config{Host: "127.0.0.1", Port: 9090}
    atomic.StorePointer(&cfgPtr, unsafe.Pointer(newCfg))

    // 读取新配置
    readCfg = (*Config)(atomic.LoadPointer(&cfgPtr))
    fmt.Println(readCfg.Host) // 127.0.0.1
}

⚠️ 使用 unsafe.Pointer 需谨慎。Go 官方推荐优先使用 atomic.Value(下节介绍)。


更通用的 atomic.Value

atomic.Value 是一个泛型容器(Go 1.18+ 前通过 interface{} 实现),可原子地存储和加载任意类型的值,无需指针转换,且类型安全。

安全地发布和读取配置

package main

import (
    "fmt"
    "sync/atomic"
)

type DBConfig struct {
    DSN  string
    Pool int
}

func main() {
    var config atomic.Value

    // 存储初始配置
    config.Store(DBConfig{DSN: "root@tcp", Pool: 10})

    // 在另一个 goroutine 中读取
    go func() {
        if cfg, ok := config.Load().(DBConfig); ok {
            fmt.Println("DSN:", cfg.DSN)
        }
    }()

    // 更新配置
    config.Store(DBConfig{DSN: "user@tcp", Pool: 20})
}

关键规则

  • 第一次 Store 决定了后续只能存储相同类型的值。
  • Load() 返回 interface{},需类型断言(或 Go 1.18+ 泛型封装)。

整数类型对应表

Go 的 atomic 包为不同整数类型提供了独立函数。以下是完整映射:

Go 类型 Load 函数 Store 函数 Add 函数 CAS 函数
int32 LoadInt32 StoreInt32 AddInt32 CompareAndSwapInt32
int64 LoadInt64 StoreInt64 AddInt64 CompareAndSwapInt64
uint32 LoadUint32 StoreUint32 AddUint32 CompareAndSwapUint32
uint64 LoadUint64 StoreUint64 AddUint64 CompareAndSwapUint64
uintptr LoadUintptr StoreUintptr AddUintptr CompareAndSwapUintptr

注意:没有 intuint 的原子操作(因平台相关,32/64 位不一致)。始终显式使用 int32/int64


实战:无锁计数器

下面是一个线程安全的计数器,支持并发自增和获取当前值:

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

type Counter struct {
    val int64
}

func (c *Counter) Increment() {
    atomic.AddInt64(&c.val, 1)
}

func (c *Counter) Value() int64 {
    return atomic.LoadInt64(&c.val)
}

func main() {
    var wg sync.WaitGroup
    counter := &Counter{}

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                counter.Increment()
            }
        }()
    }

    wg.Wait()
    fmt.Println(counter.Value()) // 必定输出 100000
}

对比使用 sync.Mutex 的版本,此实现无锁,性能更高,尤其在高并发场景。


注意事项与陷阱

  1. 不要混合原子与非原子访问
    对同一个变量,所有读写都必须通过 atomic 函数。否则仍会引发数据竞争。

  2. atomic 不是万能的
    若需原子性地更新多个字段(如账户余额和日志),必须用锁。例如:

    // ❌ 错误:两个原子操作无法保证整体原子性
    atomic.AddInt64(&balance, -100)
    atomic.AddInt64(&logCount, 1)
    
    // ✅ 正确:用互斥锁包裹
    mu.Lock()
    balance -= 100
    logCount++
    mu.Unlock()
  3. 64 位对齐问题(仅 32 位平台)
    在 32 位系统上,int64/uint64 变量必须 64 位对齐,否则 atomic 操作 panic。
    解决方案:将 int64 字段放在 struct 的第一个位置,或使用 atomic.Value

  4. CAS 循环可能忙等待
    实现无锁算法时,CAS 失败需重试,可能导致 CPU 占用高。应结合 runtime.Gosched() 或退避策略。


性能对比:atomic vs mutex

以下基准测试展示两者差异:

package main

import (
    "sync"
    "sync/atomic"
    "testing"
)

var (
    atom int64
    mu   sync.Mutex
    mtx  int64
)

func BenchmarkAtomic(b *testing.B) {
    for i := 0; i < b.N; i++ {
        atomic.AddInt64(&atom, 1)
    }
}

func BenchmarkMutex(b *testing.B) {
    for i := 0; i < b.N; i++ {
        mu.Lock()
        mtx++
        mu.Unlock()
    }
}

运行 go test -bench=. 典型结果:

BenchmarkAtomic-8    100000000    10.2 ns/op
BenchmarkMutex-8     30000000     42.1 ns/op

结论:对于单一变量的简单操作,atomicmutex 快 3-5 倍。


总结使用步骤

  1. 确定需求:是否只需操作单个整数或指针?
  2. 选择类型:明确使用 int64/uint64 等具体类型(避免 int)。
  3. 统一访问:对该变量的所有读写都通过 atomic 函数。
  4. 复杂场景回退:若涉及多变量或复杂逻辑,改用 sync.Mutex
  5. 优先 atomic.Value:对结构体或接口类型,使用 atomic.Value 避免 unsafe

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文