先说一下具体背景,本人在刷题,有一道题是要求使用三个协程依次循环输出 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 拿出来,加一后再放回去,这个拿出来是赋值给寄存器,寄存器加一后再拷贝到栈中,这个过程另一协程也会去读,同样把值赋值给寄存器,这个寄存器是一样的?共享的?所以就被覆盖了?感觉有这个可能。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.