本篇文章为大家展示了Go语言中单核CPU开两个Goroutine时其中一个死循环会怎么样,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
今天的男主角,是与 Go 工程师有调度相关的知识,那就是 “单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?”
请在此处默念自己心目中的答案,再往和煎鱼一起研讨一波 Go 的技术哲学。
针对这个问题,我们需要把问题剖开来看看,其具有以下几个元素:
根据这道题的题意,可大致理解其想要问的是 Go 调度相关的一些知识理解。
第一个要点,就是要明确 “计算机只有一个单核 CPU” 这一个变量定义,对 Go 程序会产生什么影响,否则很难继续展开。
既然明确涉及 Goroutine,这里就会考察到你对 Go 的调度模型 GMP 的基本理解了。
从单核 CPU 来看,最大的影响就是 GMP 模型中的 P,因为 P 的数量默认是与 CPU 核数(GOMAXPROCS)保持一致的。
go func
就是生成了一个 G。GOMAXPROCS
进行修改。这三者交互实际来源于 Go 的 M: N 调度模型。也就是 M 必须与 P 进行绑定,然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务。
第二个要点,就是 Goroutine 的数量和运行模式都是受限的。有两个 Goroutine,一个 Goroutine 在死循环,另外一个在正常运行。
这可以理解为 Main Goroutine + 起了一个新 Goroutine 跑着死循环,因为本身 main 函数就是一个主协程在运行着,没毛病。
需要注意的是,Goroutine 里跑着死循环,也就是时时刻刻在运行着 “业务逻辑”。这块需要与单核 CPU 关联起来,考虑是否会一直阻塞住,把整个 Go 进程运行给 hang 住了?
注:但若是在现场面试,可以先枚举出这种场景,诠释清楚后。再补充提问面试官,是否这类场景?
第三个要点,是一个隐性的拓展点。如果你是一个老 Go 粉,经常关注 Go 版本的更新情况(至少大版本),则应该会知道 Go 的调度是会变动的(会在后面的小节讲解)。
因此本文这个问题,在不同的 Go 语言版本中,结果可能会是不一样的。但是面试官并没有指出,这里就需要考虑到:
如果你注意到了,是一个小亮点,说明你在这块有一定的知识积累。
在刚刚过去的 3s 中,你已经把上面的考量都在大脑中过了一遍。接下来我们正式进入实战演练,构造一个例子:
// Main Goroutine
func main() {
// 模拟单核 CPU
runtime.GOMAXPROCS(1)
// 模拟 Goroutine 死循环
go func() {
for {
}
}()
time.Sleep(time.Millisecond)
fmt.Println("脑子进煎鱼了")
}
在上面的例子中,我们通过以下方式达到了面试题所需的目的:
runtime.GOMAXPROCS
方法模拟了单核 CPU 下只有一个 P 的场景。思考一下:这段程序是否会输出 ”脑子进煎鱼了“ 呢,为什么?
答案是:
这是怎么回事呢,这两种情况分别对应了什么原因和标准,Go 版本的变更有带来了什么影响?
显然,这段程序是有一个 Goroutine 是正在执行死循环,也就是说他肯定无法被抢占。
这段程序中更没有涉及主动放弃执行权的调用(runtime.Gosched),又或是其他调用(可能会导致执行权转移)的行为。因此这个 Goroutine 是没机会溜号的,只能一直打工...
那为什么主协程(Main Goroutine)会无法运行呢,其实原因是会优先调用休眠,但由于单核 CPU,其只有唯一的 P。唯一的 P 又一直在打工不愿意下班(执行 for 死循环,被迫无限加班)。
因此主协程永远没有机会呗调度,所以这个 Go 程序自然也就一直阻塞在了执行死循环的 Goroutine 中,永远无法下班(执行完毕,退出程序)。
那为什么 Go1.14 及以后的版本,又能正常输出了呢?
主要还是在 Go1.14 实现了基于信号的抢占式调度,以此来解决上述一些仍然无法被抢占解决的场景。
主要原理是Go 程序在启动时,会在 runtime.sighandler
方法注册并且绑定 SIGURG
信号:
func mstartm0() {
...
initsig(false)
}
func initsig(preinit bool) {
for i := uint32(0); i < _NSIG; i++ {
...
setsig(i, funcPC(sighandler))
}
}
绑定相应的 runtime.doSigPreempt
抢占方法:
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
...
if sig == sigPreempt && debug.asyncpreemptoff == 0 {
// 执行抢占
doSigPreempt(gp, c)
}
}
同时在调度的 runtime.sysmon
方法会调用 retake
方法处理一下两种场景:
该方法会检测符合场景的 P,当满足上述两个场景之一时,就会发送信号给 M。M 收到信号后将会休眠正在阻塞的 Goroutine,调用绑定的信号方法,并进行重新调度。以此来解决这个问题。
注:在 Go 语言中,sysmon 会用于检测抢占。sysmon 是 Go 的 Runtime 的系统检测器,sysmon 可进行 forcegc、netpoll、retake 等一系列骚操作(via @xiaorui)。
在这篇文章中,我们针对 ”单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?“ 这个问题进行了展开剖析。
针对不同 Go 语言版本,不同程序逻辑的表现形式都不同,但背后的基本原理都是与 Go 调度模型和抢占有关。
上述内容就是Go语言中单核CPU开两个Goroutine时其中一个死循环会怎么样,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。