内存可见性:什么情况下必须同步,什么时候不需要?

一句话结论:判定标准只有一个——是否存在“数据竞争”。多个 goroutine 并发访问同一个变量,其中至少一个是写操作,且访问之间没有任何同步(无 happens-before),就是数据竞争,必须加同步;反之,只读共享、goroutine 私有、或写入已被同步原语“发布”出去的场景都不需要额外同步。

三步判定法

  • 第一步:变量是否被多个 goroutine 共享?只在函数内使用、只作为参数向下传递而不被并发的,天然安全。
  • 第二步:共享后是否只读?所有 goroutine 都只读(初始化完成后不再修改),安全。
  • 第三步:只要有写,写入与后续读取之间是否建立了 happens-before?没有,就需要同步。

典型场景对照

场景是否安全原因
启动前赋值,go 语句启动新 goroutine 后读取安全go 语句 happens-before 新 goroutine 执行
经无缓冲 channel 把值从 A 发给 B安全发送 happens-before 接收完成
只读的全局配置(init 之后不变)安全无写操作即无竞争
计数器在多个 goroutine 中读改写不安全并发读写无同步
一个 goroutine 写、另一个读(无锁)不安全读写并发即竞争,哪怕只有一次
读之前用 Mutex/WaitGroup 确认写已完成安全已建立 happens-before

一个常见误判

很多人以为“主 goroutine 先 sleep 一会儿再读就安全”,这是错的:睡眠既不建立 happens-before,也不保证另一侧“已经写完”。正确做法是用 channel 或 WaitGroup 做明确的完成信号。

func main() {
	done := make(chan struct{})
	var msg string

	go func() {
		msg = "hello"
		close(done) // close 建立 happens-before
	}()

	<-done
	fmt.Println(msg) // 安全:能读到 "hello"
}

常见追问 / 记忆点

  • 追问:sync/atomic 的原子读能保证看到“最新”的写吗?——原子性不等于可见性排序,需要配合 release/acquire 语义才构成同步。
  • 追问:两个 goroutine 一个只读一个只写也是竞争吗?——是,并发访问中只要存在写且无同步就算数据竞争。
  • 记忆点:共享 + 可变 + 无同步 = 数据竞争;拿不准就用 go test -race 验证。
笔记加载中…