Skip to content
Go
sync/atomic — 原子操作与无锁编程

sync/atomic — 原子操作与无锁编程

sync/atomic 提供了CPU 级别的原子操作。与 sync.Mutex 不同,原子操作不涉及操作系统调度,开销极小——一条 CPU 指令完成。


1. 🔬 深入原理:什么是"原子操作"?

在 CPU 层面,“原子"意味着操作不可分割——要么完全执行,要么完全不执行,不存在中间状态被其他线程看到。

// 非原子操作(有竞态条件)
counter++  // 实际上是:read → increment → write(三步)

// 原子操作
atomic.AddInt64(&counter, 1)  // CPU 保证这一步不可分割

Go 的 atomic 操作底层依赖:

  • x86/amd64: LOCK 前缀指令(如 LOCK XADD
  • ARM64: LDADDAL 等专用原子指令

2. 原子计数器

var counter int64

// 原子加
atomic.AddInt64(&counter, 1)

// 原子读(必须用 Load,直接读不算原子)
val := atomic.LoadInt64(&counter)

// 原子写(必须用 Store)
atomic.StoreInt64(&counter, 0)

// 原子交换(写入新值、返回旧值)
old := atomic.SwapInt64(&counter, 100)

🚨 陷阱:在 32 位平台上,int64 的原子操作需要8 字节对齐。如果结构体中的 int64 没有对齐到 8 字节边界,atomic 操作会 panic。解决:把原子字段放在结构体最前面,或使用 atomic.Int64(Go 1.19+,自动保证对齐)。


3. CAS — 比较并交换(Compare And Swap)

CAS 是无锁数据结构的核心原语。

// 语义:如果 *addr == old,则 *addr = new,返回 true
// 否则不改变 *addr,返回 false
swapped := atomic.CompareAndSwapInt64(&value, old, new)

经典模式:CAS 循环

// 无锁计数器加法
func atomicInc(addr *int64) {
    for {
        old := atomic.LoadInt64(addr)
        new := old + 1
        if atomic.CompareAndSwapInt64(addr, old, new) {
            return // 成功
        }
        // 被其他 goroutine 抢先了,重试
    }
}

性能提示:高竞争下 CAS 循环可能导致大量重试(spin)。对于简单计数器,直接用 AddInt64 比 CAS 循环高效。


4. atomic.Value — 任意类型的原子存储

var config atomic.Value

// 存储
config.Store(&Config{Port: 8080})

// 读取
cfg := config.Load().(*Config)

// CAS(Go 1.17+)
config.CompareAndSwap(oldCfg, newCfg)

// Swap(Go 1.17+)
old := config.Swap(newCfg)

🚨 陷阱

  • atomic.Value 不能存储 nil(会 panic)。
  • 存储的类型必须一致(第一次 Store 后,后续 Store 必须是同类型)。
  • 对指针类型的 Value,指向的数据被修改不受原子保护。只有指针本身的读写是原子的。

5. Go 1.19+ 的类型化原子变量

Go 1.19 引入了类型安全的原子类型,强烈推荐使用

// Go 1.19 之前(容易出错)
var counter int64
atomic.AddInt64(&counter, 1)

// Go 1.19+(类型安全,自动保证对齐)
var counter atomic.Int64
counter.Add(1)
val := counter.Load()
counter.Store(0)
counter.CompareAndSwap(0, 100)
类型 对应基础类型
atomic.Bool bool
atomic.Int32 int32
atomic.Int64 int64
atomic.Uint32 uint32
atomic.Uint64 uint64
atomic.Uintptr uintptr
atomic.Pointer[T] *T(泛型)
atomic.Value 任意类型
// atomic.Bool 替代 sync.Once 实现一次性初始化
var initialized atomic.Bool

func ensureInit() {
    if initialized.Load() {
        return
    }
    // 慢路径:只有第一个到达的做初始化
    doExpensiveSetup()
    initialized.Store(true)
}

6. atomic.Pointer[T] — 泛型原子指针

type Server struct {
    addr string
    port int
}

var currentServer atomic.Pointer[Server]

// 存储
s := &Server{addr: "localhost", port: 8080}
currentServer.Store(s)

// 读取
server := currentServer.Load()
fmt.Println(server.addr, server.port)

// CAS 更新
newServer := &Server{addr: "0.0.0.0", port: 9090}
if currentServer.CompareAndSwap(s, newServer) {
    fmt.Println("updated")
}

🔬 实战:无锁栈

type LockFreeStack struct {
    head atomic.Pointer[node]
}

type node struct {
    value int
    next  *node
}

func (s *LockFreeStack) Push(v int) {
    n := &node{value: v}
    for {
        old := s.head.Load()
        n.next = old
        if s.head.CompareAndSwap(old, n) {
            return
        }
        // CAS 失败说明有人抢先了,重试
    }
}

func (s *LockFreeStack) Pop() (int, bool) {
    for {
        old := s.head.Load()
        if old == nil {
            return 0, false
        }
        if s.head.CompareAndSwap(old, old.next) {
            return old.value, true
        }
    }
}

7. 内存顺序与 atomic

🔬 深入原理:Go 的 atomic 操作隐式使用了 sequentially consistent 模型——比 C/C++ 的 memory_order_seq_cst 简单得多。这意味着:

  • atomic Store → atomic Load:Load 一定看到 Store 之后的值。
  • 不需要显式指定 memory order(Go 替你保证)。
// Go 中不需要考虑这个:
// var data int64
// data = 42
// atomic.StoreInt64(&flag, 1)
//
// 另一个 goroutine:
// if atomic.LoadInt64(&flag) == 1 {
//     fmt.Println(data) // 一定能看到 42(sequential consistency 保证)
// }

8. atomic vs Mutex:何时用哪个?

场景 推荐
简单计数 atomic
单个标志位/状态切换 atomic
读-修改-写简单值 atomic
复杂的复合操作(多字段更新) Mutex
保护一段代码而非单个变量 Mutex
需要等待条件 Mutex + Cond 或 channel