我正在努力更好地了解Go程序中的goroutine是如何调度的,尤其是在哪些点上它们可以屈服于其他goroutine。我们知道goroutine会在系统卡上产生阻塞,但显然这并不是全部情况。
这个问题也引起了类似的担忧,最受好评的答案是goroutine也可能打开函数调用,因为这样做会调用调度器来检查堆栈是否需要增长,但它明确表示
如果你没有任何函数调用,只有一些数学,那么是的,goroutine将锁定线程,直到它退出或碰到可以让其他人执行死刑。
我写了一个简单的程序来检查和证明:
package main
import "fmt"
var output [30]string // 3 times, 10 iterations each.
var oi = 0
func main() {
runtime.GOMAXPROCS(1) // Or set it through env var GOMAXPROCS.
chanFinished1 := make(chan bool)
chanFinished2 := make(chan bool)
go loop("Goroutine 1", chanFinished1)
go loop("Goroutine 2", chanFinished2)
loop("Main", nil)
<- chanFinished1
<- chanFinished2
for _, l := range output {
fmt.Println(l)
}
}
func loop(name string, finished chan bool) {
for i := 0; i < 1000000000; i++ {
if i % 100000000 == 0 {
output[oi] = name
oi++
}
}
if finished != nil {
finished <- true
}
}
注意:我知道在数组中放入一个值并在没有同步的情况下递增oi
是不太正确的,但我想让代码变得简单,并且没有可能导致切换的东西。毕竟,可能发生的最糟糕的事情是在不推进索引(覆盖)的情况下输入值,这没什么大不了的
与这个答案不同,我避免使用作为goroutine启动的loop()
函数的任何函数调用(包括内置的append()
),而且我明确设置了GOMAXPROCS=1
,根据文档:
限制可以同时执行用户级Go代码的操作系统线程数。
然而,在输出中,我仍然看到消息Main
/Goroutine 1
/Goroutine 2
交错,这意味着以下其中之一:
- goroutine的执行中断,goroutine放弃在某些时刻的控制
GOMAXPROCS
不按文档,旋转更多的操作系统线程来调度goroutines
要么答案不完整,要么自2016年以来有些事情发生了变化(我在Go 1.13.5和1.15.2上进行了测试)。
如果这个问题得到了回答,我很抱歉,但我既没有找到为什么这个特定的例子会产生控制的解释,也没有找到关于goroutines通常产生控制的点的解释(阻塞系统调用除外)。
注意:这个问题纯粹是理论性的,我现在不想解决任何实际任务,但总的来说,我认为知道goroutine可以产生和不能产生的点可以避免同步原语的冗余使用
Go版本1.14引入异步抢占:
Goroutines现在可以异步抢占。因此,没有函数调用的循环不再可能使调度程序死锁或显著延迟垃圾收集。除
windows/arm
、darwin/arm
、js/wasm
和plan9/*
外,所有平台都支持此功能。
如通道是否发送用于goroutine调度的抢占点?中所回答的?,Go的抢占点可能会随着一个版本的不同而变化。异步抢占只是在几乎所有地方添加了可能的抢占点。
对output
数组的写入没有同步,oi
索引也不是原子索引,这意味着我们不能真正确定输出数组会发生什么。当然,通过互斥添加原子性会引入协作调度点。虽然这些不是协作调度切换的来源(必须根据您的输出进行),但它们确实干扰了我们对程序的理解。
output
数组保存字符串,使用字符串可以调用垃圾收集系统,则可以使用锁并导致调度切换。因此,在Go-1.14之前的实现中,这是最有可能导致调度切换的原因。
正如@torek所指出的,GO最流行的运行时环境已经使用了几个月的抢占式调度(自1.14以来)。否则,goroutine可能产生的点会根据运行时环境和发布而有所不同,但William Kennedy给出了一个很好的总结。
我还记得几年前在编译器中添加了一个选项,为长时间运行的循环添加屈服点,但这是一个通常不会触发的实验选项。(当然,您可以通过在循环中不时调用runtime.GoSched
手动完成。)
至于你的测试,我对你在Go 1.13.5下运行时得到的结果感到惊讶。由于数据竞赛,行为没有得到确切的定义(我知道你避免了任何同步机制来避免触发收益),但我没想到会有这样的结果。一件事是,将GOMAXPROCS
设置为1将意味着只有一个goroutine同时执行,但这可能不一定意味着当不同的goroutine执行时,它将在同一个核心上运行。不同的核心将具有不同的高速缓存以及(在没有同步的情况下)对output
和oi
的值的不同意见。
但我可以建议您忘记修改全局变量,只在繁忙循环前后记录一条消息。这应该清楚地表明(在GO<1.14中)一次只运行一个lopp。(多年前,我和你做了同样的实验,结果似乎奏效了。)