package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var ok bool
cpuNu := runtime.NumCPU()
for i := 0; i < cpuNu; i++ {
go func() {
for {
if ok {
// to do
}
}
}()
}
time.Sleep(3 * time.Second)
fmt.Println("Not print...why???")
}
有疑问加站长微信联系(非本文作者)

死循环里不能啥也不干,会导致 CPU 飙升。
在写个限速的库,原本是用锁来实现的一些操作,后来改成原子操作,跑基准测试的时候发现的。试过其他语言不会这样,我想这种特殊场景暴露出goroutine调度的问题,可能是个坑。
你这个写法很有趣,要想解释清楚原因,需要对Go的runtime的抢占调度机制有个清晰的认识。
你起了当前运行时可以最大并行运行的goroutine数目,然后让main goroutine sleep之后,就是你for循环中启动的那些goroutines在实际的运行,并且占满了你的cpu core数,当你在goroutine中是一个for死循环,没有任何函数调用。
我们知道如果是OS线程的话,OS一般是基于时间片发起抢占调度(通过中断的方式),让运行时间太长的线程调度出cpu,以保证其他线程有机会被执行。同样的go语言的runtime调度器也支持抢占调度(将运行时间过长的goroutine调度出去,避免其一直运行),但由于是在用户态发起的抢占,并不能把当前正在运行的goroutine直接中断掉,所以go runtime调度器是通过为当前正在运行的goroutine设置一个flag标志其应该被抢占了(运行时间太长的原因),然后goroutine是在自己有函数调用的时候,判断一下自己的抢占flag是否被set,如果被set就主动出让CPU。
所以你的代码正好抓住了go runtime调度器的缺点,你可以在你的goroutine中的for死循环中加一个fmt.Print()试验一下。
并不是有函数调用就行,需要runtime.Gosched()。我贴一下我当时的代码,循环体内有调用函数的。
要看函数是否会被优化成内联函数,不产生具体的函数调用,是不会发生调度的
上面调用的代码不对,改了一下
去掉内联优化也一样 go build -gcflags="-N -l"
golang所谓的“有函数调用,就有了进入调度器代码的机会”,实际上是go编译器在函数的入口处插入了runtime.morestack_noctxt的函数调用,这个函数会检查是否扩容连续栈,并进入抢占调度的逻辑中。如果你调用的函数是直接可以确定函数栈的,没有插入runtime.morestack_noctxt,则不会被抢占。知道这些确实要对golang的调度器有很深的了解。