
Go 编译器和 CPU 会重排序,go 启动的 goroutine 不一定看到你“刚写完”的值
Go 的内存模型不保证普通变量写入对其他 goroutine 立即可见。编译器可能把 a = 1 提前到 done = true 前面,CPU 也可能延迟刷新缓存行——结果就是另一个 goroutine 看到 done == true,但 a 还是 0。
- 这不是 bug,是标准允许的优化:Go 只在
sync原语(如sync.Mutex、sync.WaitGroup、channel收发)或atomic操作处插入内存屏障 - 不要用
time.Sleep模拟同步——它既不可靠,又掩盖真实问题 - 即使变量加了
volatile(Go 根本没有这个关键字),也完全无效
sync/atomic 是最轻量且明确的可见性控制方式
用 atomic.StoreInt32 写,atomic.LoadInt32 读,就能强制编译器和 CPU 尊重顺序,并刷新缓存。它比 mutex 开销小,且语义清晰:你要的只是“这个整数的读写必须全局可见”。
-
atomic操作要求变量地址对齐(int32必须 4 字节对齐),结构体字段顺序会影响对齐,别把int32和byte紧挨着放 - 不能对结构体整体做
atomic.StorePointer后直接解引用字段——除非你确保整个结构体是只读的,否则仍存在字段级重排风险 -
atomic.CompareAndSwap系列自带 acquire/release 语义,适合实现无锁状态机,但注意失败时要重试逻辑
var done int32
go func() {
a = 1
atomic.StoreInt32(&done, 1) // ← 这里插入 full barrier
}()
for atomic.LoadInt32(&done) == 0 {} // ← 这里也是 barrier
// 此时 a 一定等于 1channel 发送和接收天然带 happens-before 关系,但别滥用作“可见性开关”
向 channel 发送后,接收方能看到发送前的所有内存写入——这是 Go 内存模型明确定义的。但它本质是同步点,不是内存屏障“装饰器”。
- 用
ch 通知完成,比轮询 <code>atomic.Load更符合 Go 风格,也避免忙等 - 但如果你只关心一个整数的可见性,却为它建个
chan int,就引入了调度开销和 GC 压力,得不偿失 - 关闭 channel 也建立 happens-before,但多次关闭 panic,且关闭后无法再判断“是否已关闭”而不引发竞态——要用
sync.Once或原子标志配合
为什么 mutex.Unlock() 能让变量可见,而 mutex.Lock() 不行?
Unlock 插入 release barrier,把临界区内的写入“推”到其他线程可见;Lock 插入 acquire barrier,只保证之后的读取不会被重排到锁获取之前,但不保证看到之前别人写的内容——除非那个写入也发生在对应的 Unlock 之后。
立即学习“go语言免费学习笔记(深入)”;
- 典型错误:goroutine A 写变量 +
mu.Unlock(),goroutine Bmu.Lock()后读——B 确实能看到 A 的写,因为 A 的 unlock 和 B 的 lock 构成同步对 - 但如果 B 在 A unlock 前就 lock 成功了(比如 A 还没进临界区),那 B 就看不到 A 后续的写
- 所以临界区必须包裹“写”和“读”双方,或者用更细粒度的原子操作替代
真正难的不是记住哪条规则,而是判断某个变量是否需要跨 goroutine 可见——很多 bug 来自假设“反正就一个字段,应该没问题”,结果在高负载或特定 CPU 架构下才暴露。只要涉及共享状态,先想清楚同步契约,再选工具。










