学习目标
- 理解为什么并发正确性不仅仅是“加锁”
- 掌握 Go 内存模型的核心思想
- 理解 happens-before 关系在并发中的意义
- 避免“看起来正确但实际上错误”的并发代码
一、为什么需要内存模型
很多并发 bug 并不是因为:
- 没有加锁
- 没有用 channel
而是因为:
- 对“执行顺序”的理解是错误的
- 假设了不存在的可见性保证
内存模型定义了:
- 一个 goroutine 的写操作
- 在什么时候、以什么方式
- 对另一个 goroutine 可见
工程认知:并发正确性 = 同步 + 正确的可见性。
二、一个“看起来正确”的错误示例
var ready bool
var data int
func worker() {
data = 42
ready = true
}
func main() {
go worker()
for !ready {
}
fmt.Println(data)
}直觉上:
- ready 为 true 时,data 应该已经是 42
但在 Go 内存模型下:
- 这是未定义行为
- 可能打印 0
- 甚至可能死循环
原因:没有任何 happens-before 关系。
三、什么是 happens-before
happens-before 是内存模型中的核心概念。
定义:
如果操作 A happens-before 操作 B,则:
- A 的所有写入,对 B 可见
- A 的执行顺序在 B 之前
反之:
如果没有 happens-before,Go 允许:
- 重排序
- 缓存延迟可见
四、Go 中建立 happens-before 的方式
Go 明确规定,以下操作会建立 happens-before 关系。
1. goroutine 启动
go f()go f() 之前的写操作,对 f 内是可见的。
2. channel 发送与接收
ch <- v // happens-before
v := <-ch发送 happens-before 对应的接收。
工程意义:channel 天然提供可见性保证。
3. channel 的 close
close(ch)close happens-before 接收方读到关闭状态。
4. mutex 的 Lock / Unlock
mu.Unlock()
mu.Lock()一次 Unlock happens-before 后续的 Lock。
工程意义:锁不仅互斥,还保证内存可见性。
5. WaitGroup
wg.Done() happens-before wg.Wait() 返回。
这保证了:
- Wait 返回时
- 所有 worker 的写操作都已完成并可见
6. atomic 操作
atomic 的 Load / Store / CAS 都包含内存屏障语义。
atomic.Store happens-before atomic.Load。
五、修复前面的错误示例
使用 channel 修复:
done := make(chan struct{})
func worker() {
data = 42
close(done)
}
func main() {
go worker()
<-done
fmt.Println(data)
}说明:
- close(done) happens-before 接收
- data 的写入对 main 可见
使用 mutex 修复:
mu.Lock()
data = 42
ready = true
mu.Unlock()对应读取侧也加锁,才能建立 happens-before。
六、常见 happens-before 误解
- “goroutine 早启动,就一定先执行” —— 错
- “sleep 一下就安全了” —— 错
- “读写 bool 是原子的就没问题” —— 错
工程共识:没有同步原语,就没有顺序保证。
七、内存模型与 race 的关系
- 数据竞争:多个 goroutine 未同步访问同一数据
- 内存模型:即使没有竞争,也可能不可见
结论:
- race-free 是必要条件
- 但还必须建立 happens-before
八、工程级并发编程原则
- 不要依赖执行时序的“直觉”
- 必须使用明确的同步原语
- 用 channel / mutex / atomic 表达意图
- sleep 不是同步手段
工程结论:并发正确性是“被证明的”,不是“跑通的”。
九、内存模型总结
- happens-before 是并发正确性的基石
- Go 明确规定了哪些操作建立可见性
- 所有并发代码都应建立明确的 happens-before 关系
一句话总结:
没有 happens-before,就没有并发正确性。
预告:Day 20|Go 调度器与运行时(runtime)基础
下一天将系统讲解:
- Go runtime 的职责
- goroutine 是如何被调度的
- 阻塞、抢占与系统调用
- 理解性能问题的“运行时视角”