goroutine 在什么时候可以产生?

我试图更好地理解 goroutines 在 Go 程序中是如何调度的,特别是在什么时候它们可以让步给其他 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

    }

}

注意:我知道在数组中放置一个值并在oi不同步的情况下递增并不完全正确,但我希望保持代码简单且没有可能导致切换的东西。毕竟,可能发生的最糟糕的事情是在不推进索引(覆盖)的情况下设置一个值,这没什么大不了的。


与这个答案不同,我避免使用作为 goroutine 启动的函数中的任何函数调用(包括内置append())loop(),而且我GOMAXPROCS=1根据文档明确设置:


限制可以同时执行用户级 Go 代码的操作系统线程数。


尽管如此,在输出中我仍然看到消息Main//交错Goroutine 1,Goroutine 2这意味着以下之一:


goroutine 的执行中断并且 goroutine 在某些时刻放弃了控制;

GOMAXPROCS不像文档中所说的那样工作,启动更多的操作系统线程来调度 goroutine。

要么答案不完整,要么自 2016 年以来有些事情发生了变化(我在 Go 1.13.5 和 1.15.2 上进行了测试)。


如果这个问题得到了回答,我很抱歉,但我既没有找到解释为什么这个特定的例子会产生控制,也没有找到 goroutines 一般产生控制的点(阻塞系统调用除外)。


注意:这个问题纯粹是理论上的,我现在不打算解决任何实际任务,但总的来说,我假设知道 goroutine 可以产生和不能产生的点可以让我们避免同步原语的冗余使用。


繁星coding
浏览 186回答 2
2回答

DIEA

Go 1.14 版本引入了异步抢占:Goroutines 现在是异步可抢占的。因此,没有函数调用的循环不再可能使调度程序死锁或显着延迟垃圾收集。windows/arm除、darwin/arm、js/wasm和之外的所有平台都支持此功能plan9/*。正如在通道是否为 goroutine 调度发送抢占点中所回答的那样?, Go 的抢占点可能会从一个版本到下一个版本发生变化。异步抢占几乎在任何地方都增加了可能的抢占点。您对数组的写入output未同步,并且您的oi索引不是原子的,这意味着我们无法确定输出数组会发生什么。当然,使用互斥体为其添加原子性会引入协作调度点。虽然这些不是协作调度切换的来源(必须根据您的输出发生),但它们确实扰乱了我们对程序的理解。数组保存了output字符串,使用字符串可以调用垃圾回收系统,垃圾回收系统可以使用锁,导致调度切换。因此,这是在 Go-1.14 之前的实现中调度切换的最可能原因。

开满天机

正如@torek 所指出的那样,最流行的 GO 运行时环境已经使用抢占式调度几个月了(从 1.14 开始)。否则,goroutine 可能产生的点取决于运行时环境和版本,但威廉肯尼迪给出了一个很好的总结。我还记得几年前在编译器中添加了一个选项来为长时间运行的循环添加屈服点,但这是一个通常不会触发的实验性选项。(当然,您可以通过runtime.GoSched在循环中不时调用来手动执行此操作。)至于你的测试,我对你在 Go 1.13.5 下运行时得到的结果感到惊讶。由于数据竞争,该行为并未完全定义(我知道您避免了任何同步机制以避免触发产量),但我没想到会出现这种结果。一件事是设置GOMAXPROCS为 1 意味着只有一个 goroutine 正在并发执行,但这不一定意味着当不同的 goroutine 执行时它将在同一个核心上运行。不同的核心将具有不同的缓存和(没有同步)对 和 的值的不同output意见oi。但是我可以建议你忘记修改全局变量,只在忙循环之前和之后记录一条消息。这应该清楚地表明(在 GO < 1.14 中)一次只能运行一个 lopp。(多年前我曾尝试和你做同样的实验,这似乎奏效了。)
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go