我试图更好地了解 Go 程序中 goroutines 是如何调度的,尤其是在它们可以让步给其他 goroutines 的时候。我们知道 goroutine 会产生会阻塞它的 syscals,但显然这不是全部。
这个问题引起了一些类似的担忧,最受好评的答案是 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
}
}
Run Code Online (Sandbox Code Playgroud)
注意:我知道在数组中放置一个值并在oi没有同步的情况下递增是不太正确的,但我想让代码保持简单并且没有可能导致切换的东西。毕竟,可能发生的最糟糕的事情是在不推进索引(覆盖)的情况下放置一个值,这没什么大不了的。
与此答案不同,我避免使用作为 goroutine 启动append()的loop()函数中的任何函数调用(包括内置函数),而且我还GOMAXPROCS=1根据文档明确设置了哪个:
限制可以同时执行用户级 Go 代码的操作系统线程数。
然而,在输出我仍然看到消息Main/ Goroutine 1/Goroutine 2交错,这意味着下列操作之一:
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 之前的实现中调度切换的最可能原因。