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 |