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