package main
import (
"fmt"
"sync"
)
func main() {
var i int
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
i = 10
}()
go func() {
defer wg.Done()
i += 10
}()
wg.Wait()
fmt.Printf("最终结果:%d\n", i) // 输出20
}
我在测试多协程对同一变量的操作,我想着以上两个协程的输出结果必定有20,但循环执行了5W次也没有,根据内存模型,这两个协程对同一变量的操作必定有先后,为什么没打印出20呢
golang sync.waitgroup内存可见性
- 写回答
- 好问题 0 提建议
- 关注问题
- 邀请回答
-
2条回答 默认 最新
Juan_2012 2025-10-28 20:06关注回答参考qwen
你提出的问题非常经典,涉及 Go 语言的并发内存模型、数据竞争(data race)与内存可见性 的核心概念。
🔍 问题分析
你的代码如下:
var i int var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() i = 10 }() go func() { defer wg.Done() i += 10 }() wg.Wait() fmt.Printf("最终结果:%d\n", i)你期望看到输出
20,但即使运行了 5 万次也从未出现。我们来深入剖析原因。
✅ 正确答案:这不是“内存可见性”问题,而是 未定义行为(Undefined Behavior)导致的结果不可预测
🧠 根本原因:存在数据竞争(Data Race)
两个 goroutine 同时访问同一个变量
i,且至少有一个是写操作(i = 10和i += 10都包含写),而 没有使用任何同步机制保护该共享变量 —— 尽管用了sync.WaitGroup。❌ 错误认知:
WaitGroup能保证变量同步?
✅ 正确认知:WaitGroup只用于等待协程结束,它不提供任何 原子性、顺序性或互斥访问 保障!因此:
i = 10i += 10(即i = i + 10)
这两个操作都是非原子的,并且并发执行时会互相干扰,造成 数据竞争(data race)。
⚠️ 数据竞争的后果是什么?
根据 Go 内存模型,一旦发生 data race,程序行为就是 undefined(未定义) —— 意味着:
- 不保证读到最新的值;
- 不保证操作顺序;
- 甚至可能读到“撕裂值”(torn write);
- 编译器可以自由优化,比如重排、缓存到寄存器;
- 程序可以做任何事:崩溃、死循环、返回奇怪结果……
所以你问:“为什么没打印出 20?”
其实更准确的问题应该是:“为什么还能稳定地不出现 20?”
💡 为什么几乎从不出现 20?
我们来看两种理论上可能的执行顺序:
执行顺序 结果 先 i = 10→ 再i += 10(此时 i 是 10)→ 得到 20✅ 理论上可行 先 i += 10(此时 i 是 0)→ 再i = 10→ 最终 10❌ 实际常见 i += 10读取 i=0 → 切换到i=10→ 回到i += 10继续赋值 → 写回 10🤯 中间状态被覆盖 但由于以下原因,20 几乎不会出现:
1. 调度器调度时机不确定
Goroutine 调度由 runtime 控制,无法保证哪个先运行。
2.
i += 10在汇编层面是三步操作:load i -> 读取当前值(可能是 0) add 10 -> 加法运算 store i -> 写回如果在这三步之间发生了
i = 10,就会把中间计算结果冲掉。3. 编译器和 CPU 的优化可能导致“缓存到寄存器”
例如:
- 一个 goroutine 把
i缓存在寄存器里; - 修改后才写回内存;
- 另一个 goroutine 的修改对它不可见;
这正是你认为的“内存可见性”问题,但它只是 data race 的表现之一。
✅ 如何修复?使用同步原语
要让
i += 10和i = 10安全执行并有机会得到 20,必须加锁或使用原子操作。方案一:使用
mutex(推荐)package main import ( "fmt" "sync" ) func main() { var i int var wg sync.WaitGroup var mu sync.Mutex wg.Add(2) go func() { defer wg.Done() mu.Lock() i = 10 mu.Unlock() }() go func() { defer wg.Done() mu.Lock() i += 10 mu.Unlock() }() wg.Wait() fmt.Printf("最终结果: %d\n", i) // 有可能是 20 或 10,取决于执行顺序 }✅ 输出可能是:
20:如果i=10先执行,然后i+=1010:如果i+=10先执行(i=0+10=10),然后i=10覆盖
👉 所以你能看到 20 了!这才是正常的并发逻辑。
方案二:使用
atomic(适用于简单操作)但注意:
i = 10和i += 10不能直接用atomic复合操作,除非你知道地址。对于
int64类型可用(需对齐):var ai int64 atomic.StoreInt64(&ai, 10) atomic.AddInt64(&ai, 10)但对于
int类型且混合赋值/增量操作,仍建议用mutex。
🔬 实验验证:加入 data race 检测
你可以用 Go 的竞态检测器运行程序:
go run -race main.go输出类似:
WARNING: DATA RACE Read at 0x00... by goroutine 7: main.main.func2() main.go:18 +0x3a Previous write at 0x00... by goroutine 6: main.main.func1() main.go:13 +0x3a这明确告诉你:有数据竞争!
📚 Go 内存模型关键规则回顾
“A read r of a variable c is allowed to observe a write w to c if both of the following hold:”
- w happens before r; or
- there is no happens-before order, but the race detector may catch it.
换句话说:
- 如果没有同步手段(如 mutex、channel、atomic),就不存在 “happens-before” 关系;
- 那么读操作能否看到写操作的结果是 未定义的。
而
WaitGroup.Wait()和wg.Done()之间确实建立了 happens-before:“A call to
Waitreturns after all calls toDonehave been made. Each call toDone‘happens before’ the correspondingWaitreturns.”但这只保证:
wg.Wait()之后能看到所有 在Done()之前完成的同步操作;- 但不保证
i = 10这种无保护的写入是安全的!
✅ 总结
问题 回答 为什么从不输出 20? 因为存在 data race,程序行为未定义;虽然理论上可能得到 20,但实际中常因调度和中间状态丢失而得不到。 WaitGroup 能解决内存可见性吗? ❌ 不能!它只用于等待协程结束,不提供互斥或原子性。 这是内存可见性问题吗? 是表现之一,但根源是 缺乏同步导致的数据竞争。 如何才能看到 20? 使用 mutex或atomic正确同步后,就可以观察到 20。
✅ 最佳实践建议
- ✅ 任何时候多个 goroutine 写同一变量,必须加锁或使用原子操作;
- ✅ 使用
go run -race定期检查数据竞争; - ✅ 不要依赖调度顺序或“我觉得应该能行”;
- ✅ 对于计数器优先考虑
atomic,复杂逻辑用mutex; - ✅
WaitGroup只用于协调生命周期,不用于保护共享状态。
本回答被题主选为最佳回答 , 对您是否有帮助呢?解决 无用评论 打赏 举报