showliuzp 2025-10-28 18:12 采纳率: 84.3%
浏览 5
已结题

golang sync.waitgroup内存可见性


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
  • 写回答

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 = 10i += 10 都包含写),而 没有使用任何同步机制保护该共享变量 —— 尽管用了 sync.WaitGroup

    ❌ 错误认知:WaitGroup 能保证变量同步?
    ✅ 正确认知:WaitGroup 只用于等待协程结束,它不提供任何 原子性、顺序性或互斥访问 保障!

    因此:

    • i = 10
    • i += 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        -> 读取当前值(可能是 0add 10        -> 加法运算
    store i       -> 写回
    

    如果在这三步之间发生了 i = 10,就会把中间计算结果冲掉。

    3. 编译器和 CPU 的优化可能导致“缓存到寄存器”

    例如:

    • 一个 goroutine 把 i 缓存在寄存器里;
    • 修改后才写回内存;
    • 另一个 goroutine 的修改对它不可见;

    这正是你认为的“内存可见性”问题,但它只是 data race 的表现之一。


    ✅ 如何修复?使用同步原语

    要让 i += 10i = 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+=10
    • 10:如果 i+=10 先执行(i=0+10=10),然后 i=10 覆盖

    👉 所以你能看到 20 了!这才是正常的并发逻辑。


    方案二:使用 atomic(适用于简单操作)

    但注意:i = 10i += 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 内存模型关键规则回顾

    来自 The Go Memory Model

    “A read r of a variable c is allowed to observe a write w to c if both of the following hold:”

    1. w happens before r; or
    2. 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 Wait returns after all calls to Done have been made. Each call to Done ‘happens before’ the corresponding Wait returns.”

    但这只保证:

    • wg.Wait() 之后能看到所有 Done() 之前完成的同步操作
    • 但不保证 i = 10 这种无保护的写入是安全的!

    ✅ 总结

    问题回答
    为什么从不输出 20?因为存在 data race,程序行为未定义;虽然理论上可能得到 20,但实际中常因调度和中间状态丢失而得不到。
    WaitGroup 能解决内存可见性吗?❌ 不能!它只用于等待协程结束,不提供互斥或原子性。
    这是内存可见性问题吗?是表现之一,但根源是 缺乏同步导致的数据竞争
    如何才能看到 20?使用 mutexatomic 正确同步后,就可以观察到 20。

    ✅ 最佳实践建议

    1. ✅ 任何时候多个 goroutine 写同一变量,必须加锁或使用原子操作;
    2. ✅ 使用 go run -race 定期检查数据竞争;
    3. ✅ 不要依赖调度顺序或“我觉得应该能行”;
    4. ✅ 对于计数器优先考虑 atomic,复杂逻辑用 mutex
    5. WaitGroup 只用于协调生命周期,不用于保护共享状态。

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

问题事件

  • 系统已结题 11月5日
  • 已采纳回答 10月28日
  • 创建了问题 10月28日