Day 19|Go 内存模型与 happens-before 关系[Go 语言 30 天系统学习计划]

学习目标

  • 理解为什么并发正确性不仅仅是“加锁”
  • 掌握 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 是如何被调度的
  • 阻塞、抢占与系统调用
  • 理解性能问题的“运行时视角”

发表评论