由于 Go 语言是协作式的调度,不会像线程那样,在时间片用完后,由 CPU 中断任务强行将其调度走。对于 Go 语言中运行时间过长的 goroutine,Go scheduler 有一个后台线程在持续监控,一旦发现 goroutine 运行超过 10 ms,会设置 goroutine 的“抢占标志位”,之后调度器会处理。但是设置标志位的时机只有在函数“序言”部分,对于没有函数调用的就没有办法了。

    Golang implements a co-operative partially preemptive scheduler.

    所以在某些极端情况下,会掉进一些陷阱。下面这个例子来自参考资料【scheduler 的陷阱】。

    1. func main() {
    2. var x int
    3. threads := runtime.GOMAXPROCS(0)
    4. for i := 0; i < threads; i++ {
    5. go func() {
    6. for { x++ }
    7. }()
    8. }
    9. time.Sleep(time.Second)
    10. fmt.Println("x =", x)
    11. }

    运行结果是:在死循环里出不来,不会输出最后的那条打印语句。

    为什么?上面的例子会启动和机器的 CPU 核心数相等的 goroutine,每个 goroutine 都会执行一个无限循环。

    创建完这些 goroutines 后,main 函数里执行一条 time.Sleep(time.Second) 语句。Go scheduler 看到这条语句后,简直高兴坏了,要来活了。这是调度的好时机啊,于是主 goroutine 被调度走。先前创建的 threads 个 goroutines,刚好“一个萝卜一个坑”,把 M 和 P 都占满了。

    在这些 goroutine 内部,又没有调用一些诸如 channeltime.sleep 这些会引发调度器工作的事情。麻烦了,只能任由这些无限循环执行下去了。

    解决的办法也有,把 threads 减小 1:

    1. func main() {
    2. var x int
    3. threads := runtime.GOMAXPROCS(0) - 1
    4. for i := 0; i < threads; i++ {
    5. go func() {
    6. for { x++ }
    7. }()
    8. }
    9. time.Sleep(time.Second)
    10. fmt.Println("x =", x)
    11. }

    运行结果:

    1. x = 0

    不难理解了吧,主 goroutine 休眠一秒后,被 go schduler 重新唤醒,调度到 M 上继续执行,打印一行语句后,退出。主 goroutine 退出后,其他所有的 goroutine 都必须跟着退出。所谓“覆巢之下 焉有完卵”,一损俱损。

    至于为什么最后打印出的 x 为 0,之前的文章《曹大谈内存重排》里有讲到过,这里不再深究了。

    还有一种解决办法是在 for 循环里加一句:

    1. go func() {
    2. time.Sleep(time.Second)
    3. for { x++ }
    4. }()

    同样可以让 main goroutine 有机会调度执行。