内存可见性:什么情况下必须同步,什么时候不需要?
一句话结论:判定标准只有一个——是否存在“数据竞争”。多个 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 验证。