V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
contradictspiral
V2EX  ›  Go 编程语言

golang WaitGroup 问题求指教

  •  
  •   contradictspiral · 2022-06-14 22:48:05 +08:00 · 1700 次点击
    这是一个创建于 924 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一直以为下面这段代码的输出结果会是 v1:25, v2:15 ,但是跑了几次发现结果出现了 20, 14; 23, 15; 20, 11 等等的随机结果,有点凌乱... 有大佬指出下是什么问题么

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    func main() {
    	var wg sync.WaitGroup
    
    	intSlice := []int{1, 2, 3, 4, 5}
    	wg.Add(len(intSlice))
    
    	v1, v2 := 0, 0
    	for _, v := range intSlice {
    		vv := v
    		go func() {
    			defer wg.Done()
    			v1 += v
    			v2 += vv
    		}()
    	}
    	wg.Wait()
    	fmt.Printf("v1:%v, v2:%v \n", v1, v2)
    }
    
    
    第 1 条附言  ·  2022-06-14 23:29:45 +08:00
    有些明白了,v1 输出值变化是协程执行顺序和值传递的问题,v2 的输出值变化是并发时赋值操作非原子操作导致的竞态问题,感谢各位大佬指教。
    第 2 条附言  ·  2022-06-14 23:53:47 +08:00
    准确的说,应该是 v1 和 v2 都存在竞态的问题,用 go run --race 来跑看到警告了
    11 条回复    2022-06-15 20:43:22 +08:00
    dzdh
        1
    dzdh  
       2022-06-14 22:54:45 +08:00
    加锁。协程不是顺序执行的。
    BBCCBB
        2
    BBCCBB  
       2022-06-14 22:58:22 +08:00
    WaitGroup 只是一个等待多个逻辑执行完成的同步工具, 他没有执行顺序性的功能.
    BBCCBB
        3
    BBCCBB  
       2022-06-14 23:00:38 +08:00
    你这里 v 是 for 循环里的变量, 在遍历过程中会变化, 协程执行时机不确定, 不能保证执行的时候 v 的值是多少.
    Buges
        4
    Buges  
       2022-06-14 23:00:55 +08:00 via Android   ❤️ 1
    go 所有地方都是值传递,但 closure 捕获变量是按引用捕获的。
    https://go.dev/doc/faq#closures_and_goroutines
    还有你 vv 变量的读写也存在 data race 。
    BBCCBB
        5
    BBCCBB  
       2022-06-14 23:02:35 +08:00   ❤️ 1
    而且你 v1, v2 在多个协程里直接加减, 都没一个锁来保护, 涉及到内存可见性的问题?
    Contextualist
        6
    Contextualist  
       2022-06-14 23:05:51 +08:00
    跟 wait group 没关系,是闭包的问题。那个匿名函数是闭包,又因为不是同步执行的,它执行的时候会访问到外面已经改变的值。为确保每个值都传递到得这样:
    go func(v, vv int) {
    ...
    }(v, vv)
    yaott2020
        7
    yaott2020  
       2022-06-15 08:37:49 +08:00 via Android
    v1 v2 加个通道吧
    fo0o7hU2tr6v6TCe
        8
    fo0o7hU2tr6v6TCe  
       2022-06-15 11:15:47 +08:00
    个人理解
    for ... range 的话,返回的 v 是个新建的一个地址,后续遍历的每个值都被赋在这个地址上, 你在 goroutine 里面用的话, 他是取的地址上的值 又因为你是起了等长数量的 goroutine, 执行的时候是无序的,在短时间片内该地址上的值是一样的,这就造成累加后的值不是 25 了
    而 vv 变量, 在 vv:=v 的时候每次都会给 vv 重新创建了一个地址,无论 goroutine 怎么乱序读,slice 对应到的 vv 值都是不同地址的, 值也是不同的
    -----

    但有一点比较好奇,解决了地址的问题,为什么还是会出现这样的情况

    func main() {
    var wg sync.WaitGroup

    intSlice := []int{1, 2, 3, 4, 5}
    wg.Add(len(intSlice))

    v1, v2 := 0, 0
    for _, v := range intSlice {
    vv := v
    go func(v, vv int) {
    defer wg.Done()
    v1 += v
    v2 += vv
    }(v, vv)
    }
    wg.Wait()
    fmt.Printf("v1:%v, v2:%v \n", v1, v2)
    }

    v1:15, v2:15
    v1:15, v2:15
    v1:15, v2:15
    v1:10, v2:10
    v1:13, v2:13
    v1:15, v2:15
    v1:15, v2:15

    ----
    contradictspiral
        9
    contradictspiral  
    OP
       2022-06-15 16:13:44 +08:00   ❤️ 1
    @hzjseasea
    这个问题根本原因应该在于多个 goroutine 同时对 v1 和 v2 进行赋值导致的竞态问题,普通的赋值操作并不是一个原子操作。
    可以看下这篇文章: https://cloud.tencent.com/developer/article/1489456
    fo0o7hU2tr6v6TCe
        10
    fo0o7hU2tr6v6TCe  
       2022-06-15 16:29:04 +08:00
    @contradictspiral 谢谢,学习了!
    FrankAdler
        11
    FrankAdler  
       2022-06-15 20:43:22 +08:00 via iPhone
    另外使用习惯上,如果 wg 会脱离当前的线程(协程)尽量传递引用(新开线程,传递,匿名函数),能避免很多潜在的问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1608 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:45 · PVG 00:45 · LAX 08:45 · JFK 11:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.