golang WaitGroup 问题求指教

2022-06-14 22:48:05 +08:00
 contradictspiral

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

1697 次点击
所在节点    Go 编程语言
11 条回复
dzdh
2022-06-14 22:54:45 +08:00
加锁。协程不是顺序执行的。
BBCCBB
2022-06-14 22:58:22 +08:00
WaitGroup 只是一个等待多个逻辑执行完成的同步工具, 他没有执行顺序性的功能.
BBCCBB
2022-06-14 23:00:38 +08:00
你这里 v 是 for 循环里的变量, 在遍历过程中会变化, 协程执行时机不确定, 不能保证执行的时候 v 的值是多少.
Buges
2022-06-14 23:00:55 +08:00
go 所有地方都是值传递,但 closure 捕获变量是按引用捕获的。
https://go.dev/doc/faq#closures_and_goroutines
还有你 vv 变量的读写也存在 data race 。
BBCCBB
2022-06-14 23:02:35 +08:00
而且你 v1, v2 在多个协程里直接加减, 都没一个锁来保护, 涉及到内存可见性的问题?
Contextualist
2022-06-14 23:05:51 +08:00
跟 wait group 没关系,是闭包的问题。那个匿名函数是闭包,又因为不是同步执行的,它执行的时候会访问到外面已经改变的值。为确保每个值都传递到得这样:
go func(v, vv int) {
...
}(v, vv)
yaott2020
2022-06-15 08:37:49 +08:00
v1 v2 加个通道吧
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
2022-06-15 16:13:44 +08:00
@hzjseasea
这个问题根本原因应该在于多个 goroutine 同时对 v1 和 v2 进行赋值导致的竞态问题,普通的赋值操作并不是一个原子操作。
可以看下这篇文章: https://cloud.tencent.com/developer/article/1489456
fo0o7hU2tr6v6TCe
2022-06-15 16:29:04 +08:00
@contradictspiral 谢谢,学习了!
FrankAdler
2022-06-15 20:43:22 +08:00
另外使用习惯上,如果 wg 会脱离当前的线程(协程)尽量传递引用(新开线程,传递,匿名函数),能避免很多潜在的问题

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/859640

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX