Golang Go语言中WaitGroup问题求指教

发布于 1周前 作者 ionicwang 来自 Go语言

一直以为下面这段代码的输出结果会是 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)

}


Golang Go语言中WaitGroup问题求指教

更多关于Golang Go语言中WaitGroup问题求指教的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

加锁。协程不是顺序执行的。

更多关于Golang Go语言中WaitGroup问题求指教的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


WaitGroup 只是一个等待多个逻辑执行完成的同步工具, 他没有执行顺序性的功能.

你这里 v 是 for 循环里的变量, 在遍历过程中会变化, 协程执行时机不确定, 不能保证执行的时候 v 的值是多少.

go 所有地方都是值传递,但 closure 捕获变量是按引用捕获的。
https://go.dev/doc/faq#closures_and_goroutines
还有你 vv 变量的读写也存在 data race 。

而且你 v1, v2 在多个协程里直接加减, 都没一个锁来保护, 涉及到内存可见性的问题?

跟 wait group 没关系,是闭包的问题。那个匿名函数是闭包,又因为不是同步执行的,它执行的时候会访问到外面已经改变的值。为确保每个值都传递到得这样:
go func(v, vv int) {

}(v, vv)

v1 v2 加个通道吧

个人理解
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

----


这个问题根本原因应该在于多个 goroutine 同时对 v1 和 v2 进行赋值导致的竞态问题,普通的赋值操作并不是一个原子操作。
可以看下这篇文章: https://cloud.tencent.com/developer/article/1489456

谢谢,学习了!

另外使用习惯上,如果 wg 会脱离当前的线程(协程)尽量传递引用(新开线程,传递,匿名函数),能避免很多潜在的问题

在Go语言中,sync.WaitGroup 是一个用于等待一组 goroutine 完成的计数器。它通常用于协调多个 goroutine 的执行,确保它们在主 goroutine 继续执行之前完成。

使用 WaitGroup 时,你需要执行以下步骤:

  1. 初始化:通过 var wg sync.WaitGroup 声明一个 WaitGroup 变量。

  2. 增加计数器:在启动每个 goroutine 之前,使用 wg.Add(1) 增加计数器。计数器的值表示需要等待完成的 goroutine 数量。

  3. 在 goroutine 中调用 Done:在每个 goroutine 结束时,调用 wg.Done() 来减少计数器。这表示一个 goroutine 已经完成。

  4. 等待完成:在主 goroutine 中,调用 wg.Wait() 阻塞,直到计数器变为零,表示所有 goroutine 都已完成。

示例代码如下:

var wg sync.WaitGroup

func worker(id int) {
    defer wg.Done() // 确保在函数结束时调用 Done
    fmt.Printf("Worker %d starting\n", id)
    // 模拟工作
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i)
    }
    wg.Wait() // 等待所有 worker 完成
    fmt.Println("All workers done")
}

确保正确使用 defer wg.Done() 以避免忘记调用 Done 方法,从而导致 Wait 永远阻塞。希望这能帮助你理解 WaitGroup 的使用!

回到顶部