你以为你写的代码会按顺序执行?在多核 CPU 下,你的直觉可能正在出卖你。
先问自己:你真的懂内存模型吗?
来看这段代码,你觉得最后会打印什么?
var x intvar done bool// goroutine Agofunc() { x = 1 done = true}()// goroutine B(轮询等待)for !done {}fmt.Println(x) // 你觉得一定会打印 1?
答案是:你不能确定打印 1。甚至在某些情况下,这个 for 循环可能永远不会退出。
为什么?这就是内存模型要解决的问题。
内存模型是什么?
一句话定义:
内存模型,就是语言对你承诺:在并发场景下,一个 goroutine 写入的数据,在什么条件下能被另一个 goroutine 观察到。
听起来很简单,但它背后涉及三个现实:
现实一:CPU 有多核,每个核有自己的缓存(Cache)
┌─────────┐ ┌─────────┐ │ Core A │ │ Core B │ │ Cache A │ │ Cache B │ └────┬─────┘ └────┬─────┘ │ │ ┌────▼───────────────────▼────┐ │ 主内存 (RAM) │ └─────────────────────────────┘
Core A 写入的数据,可能先在 Cache A 里,还没同步到主内存,Core B 就看不到。
现实二:编译器会重排指令
你写的顺序:
写 x → 写 done
编译器可能优化为:
写 done → 写 x
现实三:CPU 本身也会重排执行顺序
即使编译器没动,硬件层面也可能乱序执行指令以提高吞吐。
这三个现实加在一起,就是为什么开头那段代码不可靠。
Go 内存模型的核心思想:Happens-Before
Go 官方文档给出了一个核心概念来解决上面的混乱:
如果事件 A “happens before” 事件 B,那么 A 的写入对 B 是可见的。
简单来说就是:你必须用语言提供的同步机制,来建立明确的 happens-before 关系,否则你无法保证数据对其他 goroutine 可见。
没有 happens-before 关系 = 行为未定义 = 你的代码有潜在的 bug。
哪些东西能建立 Happens-Before?
Go 给你提供了几种工具,每种都能建立确定的 happens-before 关系:
1. Channel(信道)
发送 → happens before → 对应的接收完成信道关闭 → happens before → 接收到零值
var x intch := make(chanstruct{})gofunc() { x = 1// ①写 x ch <- struct{}{} // ②发送}()<-ch // ③接收(happens after ②)fmt.Println(x) // ④读 x →一定能看到 1 ✓
发送发生在接收之前,而写 x 发生在发送之前,所以写 x happens-before 读 x。链条建立,保证了可见性。
2. Mutex(互斥锁)
第 n 次 Unlock → happens before → 第 n+1 次 Lock
var mu sync.Mutexvar x intgofunc() { mu.Lock() x = 1 mu.Unlock() // ① Unlock}()mu.Lock() // ② Lock(happens after ①)fmt.Println(x) // 一定能看到 1 ✓mu.Unlock()
3. sync.Once
once.Do(f) 中 f 的执行 → happens before → 任何 once.Do 返回
var once sync.Oncevar x intinitX := func() { x = 42// 只执行一次}// 无论多少个 goroutine 同时调用,都能安全读到 42gofunc() { once.Do(initX); fmt.Println(x) }()gofunc() { once.Do(initX); fmt.Println(x) }()gofunc() { once.Do(initX); fmt.Println(x) }()
4. atomic(原子操作)
atomic 写入 → happens before → 后续的 atomic 读取(对同一变量)
var x intvar done atomic.Boolgofunc() { x = 1 done.Store(true) // atomic 写入}()for !done.Load() {} // atomic 读取fmt.Println(x) // 看到 1 ✓
回到开头那段代码,把 done 改成 atomic.Bool 就能修复。
回到开头:为什么那段代码出问题?
var x intvar done bool// ←普通变量,没有任何同步机制gofunc() { x = 1 done = true// 写入可能被重排到 x = 1 之前}()for !done {} // 编译器可能把 done 缓存在寄存器里,永远读不到更新fmt.Println(x)
问题所在:
done 是普通变量,没有建立任何 happens-before 关系- 即使
done 变成了 true,x = 1 也不一定对另一个 goroutine 可见
修复:换用 channel 或 atomic
// ✅用 channelch := make(chanstruct{})gofunc() { x = 1 ch <- struct{}{}}()<-chfmt.Println(x) // 安全✓// ✅用 atomicvar done atomic.Boolgofunc() { x = 1 done.Store(true)}()for !done.Load() {}fmt.Println(x) // 安全✓
一图记住全部
你写的代码的执行顺序 ↓ 可能被编译器重排 + CPU 重排 + 缓存不一致 ↓ 导致goroutine 之间数据不可见 ↓ 解决方案是用 happens-before 机制建立同步 ↓ Go 提供的工具Channel / Mutex / Once / Atomic
最后的忠告
在 Go 中,如果你在多个 goroutine 之间共享变量,你必须使用同步机制。不要依赖”它在我电脑上能正常运行”来判断代码的正确性——那可能只是因为你的机器顺序执行了指令。
记住一个原则:共享变量 + 多个 goroutine = 必须同步。没有例外。
本文参考 Go 官方内存模型文档:https://go.dev/ref/mem