诡异的执行结果,有哪位 Go 大神来给瞧瞧?

2022-06-01 00:22:35 +08:00
 cocong

先说一下具体背景,本人在刷题,有一道题是要求使用三个协程依次循环输出 ABCABCABCABCABC 。

以下这种实现方式会出现非常诡异的结果:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup = sync.WaitGroup{}
    wg.Add(1)

    // var ch chan bool = make(chan bool)
    var i int = 0

    go func() {
        for {
            // 自旋锁
            for i%3 != 0 {
            }

            fmt.Print("A", i)

            i = i + 1
        }
    }()
    go func() {
        for {
            // 自旋锁
            for i%3 != 1 {
            }

            fmt.Print("B", i)

            i = i + 1
        }
    }()
    go func() {
        for {

			// 限制循环次数,避免一直死循环
            if i >= 3 {
                fmt.Print("E", i, "\n")
                i = 2
                break
            }

            // 这段如果注释掉,就只会输出 AB 然后一直死循环
            fmt.Print("[K]")

            // 自旋锁
            for i%3 != 2 {
            }

            fmt.Print("C", i)

            i++

        }
        wg.Done()
    }()

    // ch <- true

    wg.Wait()
}

上面三个协程使用一个变量来模拟锁,当变量的值和自身对应,即和 3 取余后比较与第 N (取 0 、1 、2 )个协程相等,就说明该协程获取到锁,于是输出对应的字母,然后通过将变量的值增加的方式来模拟释放锁。

如果直接运行上面那段代码,有时候会输出

[K]A0B1C2E3
A3A3B4

为了方便查找问题,在输出字母的时候也会同时输出 i 的值,可以看到有两个 A3 ,问题是每次协程输出字母后 i 的值都会自增,理论上不可能出现两个 A3 ,但显示就是这么诡异。

还有,代码注释里面又说到,如果把 fmt.Print("[K]"),注释掉,就只会输出 A0B1 ,然后一直陷入死循环。真实诡异!

这还没完,如果把 if i >= 3 { 这段用来限制循环次数的代码放到 fmt.Print("C", i) 下面,那一切又恢复正常了。负负得正?诡异的诡异为正常?

本人的 Go 版本为 1.18.1 ,切换到 1.14.15 也是有同样的问题。

个人猜测是 i = i + 1 的问题,于是在 i = i + 1 后也再输出 i 的值,发现 i 的值并有增加,这样看来确实是它的问题,问题这没道理啊!虽说三个协程存在并发问题,但在操作 i 时只有一个协程在操作,其它都是在读,不应该会影响才对。难道真的有影响?一个协程把 i 拿出来,加一后再放回去,这个拿出来是赋值给寄存器,寄存器加一后再拷贝到栈中,这个过程另一协程也会去读,同样把值赋值给寄存器,这个寄存器是一样的?共享的?所以就被覆盖了?感觉有这个可能。

3764 次点击
所在节点    Go 编程语言
27 条回复
zealllot
2022-06-01 16:02:04 +08:00
没懂为啥把“E”去掉就死循环了,我本地跑没有复现,跑的结果是好的,ABCABC……
LeegoYih
2022-06-01 18:31:35 +08:00
```go
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
a, b, c := make(chan int, 1), make(chan int, 1), make(chan int, 1)
p := func(cur, next chan int, v byte) {
defer wg.Done()
for i := 0; i < 100; i++ {
<-cur
fmt.Printf("%c", v)
next <- 1
}
}
a <- 1
go p(a, b, 'A')
go p(b, c, 'B')
go p(c, a, 'C')
wg.Wait()
}
```
kiwi95
2022-06-01 18:55:12 +08:00
这样写显然存在 data race ,修好了应该没问题
wqtacc
2022-06-02 00:01:36 +08:00
```go
package main

func main() {
chs := []chan struct{}{
make(chan struct{}), make(chan struct{}), make(chan struct{}),
}
next := make(chan struct{})
for i := 0; i < len(chs); i++ {
go func(i int) {
for range chs[i] {
b := byte('A' + i)
print(string(b))
if i != len(chs)-1 {
chs[i+1] <- struct{}{}
} else {
next <- struct{}{}

}
}
}(i)
}
for i := 0; i < 10; i++ {
chs[0] <- struct{}{}
<-next
}
}
```
katsusan
2022-06-03 10:16:15 +08:00
for i%3 !=2 被编译器优化后不会每次循环再 load i.
可以在循环体里或者 fmt.Println("K")那里放一个空函数, 或者编译时-gcflags="-N"禁用部分优化都能避免 case3 的死循环.

你的代码中每个协程里 load 或 store i 的地方都应该用 atomic.Load/Store 操作, 不仅是为了暗示编译器不能优化该处
load/store 操作(类似于其它语言的 volatile 语义), 同时也避免乱序出现匪夷所思的输出.
lysS
2022-06-08 14:06:50 +08:00
i = i + 1 不是原子的, i 可能变成任何值
wh1012023498
2022-06-18 23:10:56 +08:00
```
package main

import "fmt"

func main() {
intCh := make(chan int)
exit := make(chan bool)

a := func() {
fmt.Print("A")
}

b := func() {
fmt.Print("B")
}

c := func() {
fmt.Print("C")
}

go func() {
for i := 1; i < 10; i++ {
intCh <- i
}
close(intCh)
}()

go func() {
for {
select {
case i := <-intCh:
if i == 0 {
exit <- true
} else {
switch i % 3 {
case 1:
a()
case 2:
b()
case 0:
c()
}
}

}
}
}()

<-exit
}
```

= = 感觉用 chan 会更好点。。waitgroup = = 这个 总感觉 在控制多个 routine 上费劲。

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

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

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

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

© 2021 V2EX