学习目标
- 理解 atomic 在 Go 并发模型中的定位
- 掌握常见原子操作的正确使用方式
- 理解内存可见性与顺序保证的基本概念
- 避免“滥用无锁”的工程误区
一、为什么需要 sync/atomic
在 Day 17 中你已经看到:
- mutex 可以解决大多数并发安全问题
- 但 mutex 有锁竞争和上下文切换成本
sync/atomic 提供的是:
- 更轻量的并发原语
- 基于 CPU 原子指令
- 避免显式加锁
重要认知:
atomic 是为“非常小的共享状态”准备的工具,而不是 mutex 的替代品。
二、什么是原子操作
原子操作指的是:
在并发场景下,一个操作要么完整执行,要么完全不执行,中间状态对其它 goroutine 不可见。
示例问题:
count++这并不是原子操作,而是:
- 读取 count
- 加 1
- 写回 count
atomic 提供的是“一条指令级别”的安全操作。
三、atomic 包的基本操作
atomic 位于:
import "sync/atomic"常见操作:
原子加:
atomic.AddInt64(&counter, 1)原子读:
v := atomic.LoadInt64(&counter)原子写:
atomic.StoreInt64(&counter, 10)关键规则:
- 参数必须是指针
- 变量必须是 64 位对齐(Go 会保证)
四、CAS(Compare-And-Swap)机制
CAS 是 atomic 的核心思想之一。
含义:
“如果当前值等于期望值,则更新为新值”
示例:
ok := atomic.CompareAndSwapInt64(&x, old, new)返回值:
- true:交换成功
- false:当前值不等于 old
CAS 是实现:
- 无锁计数器
- 状态机
- 轻量级同步
的基础。
五、atomic.Value:原子存取复杂对象
atomic.Value 用于原子地存取一个值(interface{})。
示例:
var v atomic.Value
v.Store(map[string]int{"a": 1})
m := v.Load().(map[string]int)
使用规则:
- 第一次 Store 决定类型
- 之后 Store 必须是同一具体类型
- Load 永远是原子的
工程场景:
- 配置热更新
- 读多写少的共享对象
六、atomic 与内存可见性
atomic 操作不仅是“不可分割”,还保证:
- 内存可见性
- 一定程度的顺序性
这意味着:
- 一个 goroutine 的 atomic.Store
- 对另一个 goroutine 的 atomic.Load 是可见的
工程结论:atomic 是 Go 并发内存模型的一部分,而不是简单的工具函数。
七、atomic 的典型使用场景
- 计数器(QPS、请求数)
- 状态标志位(启动/关闭)
- 读多写少的全局配置(atomic.Value)
不适合 atomic 的场景:
- 复杂临界区
- 多个字段需要一致性
- 涉及条件判断与流程控制
八、atomic vs mutex 的工程取舍
选择 atomic 当:
- 操作极其简单
- 性能瓶颈明确在锁竞争
- 代码可读性仍然可接受
选择 mutex 当:
- 逻辑复杂
- 需要维护不变量
- 团队可维护性优先
工程共识:
能用 mutex 清晰解决的问题,不要强行 atomic。
九、无锁编程的常见误区
- 认为无锁一定比加锁快
- 多个 atomic 操作组合成“伪原子逻辑”
- 忽视代码可读性与维护成本
工程结论:无锁是高级工具,不是默认选择。
十、atomic 总结
- atomic 提供底层、轻量级并发原语
- atomic 解决的是“单点状态”的并发安全
- atomic 是 mutex 的补充,而不是替代
一句话总结:
atomic 用得好是利器,用不好是灾难。
预告:Day 19|内存模型与 happens-before 关系
下一天将系统讲解:
- Go 内存模型的核心规则
- happens-before 的真实含义
- 为什么“看起来正确”的并发代码仍然是错的
- 如何写出符合内存模型的代码