Go语言中单核CPU开两个Goroutine时其中一个死循环会怎么样

本篇文章为大家展示了Go语言中单核CPU开两个Goroutine时其中一个死循环会怎么样,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

从2013年创立创新互联专注于”帮助中小企业+互联网”, 也是目前成都地区具有实力的互联网服务商。团队致力于为企业提供--站式网站建设、移动端应用( H5手机营销、手机APP定制开发、微信开发)、软件开发、信息化解决方案等服务。

今天的男主角,是与 Go 工程师有调度相关的知识,那就是 “单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?

请在此处默念自己心目中的答案,再往和煎鱼一起研讨一波 Go 的技术哲学。

问题定义

针对这个问题,我们需要把问题剖开来看看,其具有以下几个元素:

  • 运行 Go 程序的计算机只有一个单核 CPU。
  • 两个 Goroutine 在运行。
  • 一个 Goroutine 死循环。

根据这道题的题意,可大致理解其想要问的是 Go 调度相关的一些知识理解。

单核 CPU

第一个要点,就是要明确 “计算机只有一个单核 CPU” 这一个变量定义,对 Go 程序会产生什么影响,否则很难继续展开。

既然明确涉及 Goroutine,这里就会考察到你对 Go 的调度模型 GMP 的基本理解了。

从单核 CPU 来看,最大的影响就是 GMP 模型中的 P,因为 P 的数量默认是与 CPU 核数(GOMAXPROCS)保持一致的。

  • G:Goroutine,实际上我们每次调用     go func 就是生成了一个 G。
  • P:Processor,处理器,一般 P 的数量就是处理器的核数,可以通过     GOMAXPROCS 进行修改。
  • M:Machine,系统线程。

这三者交互实际来源于 Go 的 M: N 调度模型。也就是 M 必须与 P 进行绑定,然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务。

Goroutine 受限

第二个要点,就是 Goroutine 的数量和运行模式都是受限的。有两个 Goroutine,一个 Goroutine 在死循环,另外一个在正常运行。

这可以理解为 Main Goroutine + 起了一个新 Goroutine 跑着死循环,因为本身 main 函数就是一个主协程在运行着,没毛病。

需要注意的是,Goroutine 里跑着死循环,也就是时时刻刻在运行着 “业务逻辑”。这块需要与单核 CPU 关联起来,考虑是否会一直阻塞住,把整个 Go 进程运行给 hang 住了

注:但若是在现场面试,可以先枚举出这种场景,诠释清楚后。再补充提问面试官,是否这类场景?

 

Go 版本的问题

第三个要点,是一个隐性的拓展点。如果你是一个老 Go 粉,经常关注 Go 版本的更新情况(至少大版本),则应该会知道 Go 的调度是会变动的(会在后面的小节讲解)。

因此本文这个问题,在不同的 Go 语言版本中,结果可能会是不一样的。但是面试官并没有指出,这里就需要考虑到:

  1. 面试官故意不指出,等着你指出。
  2. 面试官没留意到这块,没想那么多。
  3. 面试官自己都不知道这块的 “新” 知识,他的知识可能还是老的。

如果你注意到了,是一个小亮点,说明你在这块有一定的知识积累。

 

实战演练

在刚刚过去的 3s 中,你已经把上面的考量都在大脑中过了一遍。接下来我们正式进入实战演练,构造一个例子:

// Main Goroutine 
func main() {
    // 模拟单核 CPU
    runtime.GOMAXPROCS(1)
    
    // 模拟 Goroutine 死循环
    go func() {
        for {
        }
    }()

    time.Sleep(time.Millisecond)
    fmt.Println("脑子进煎鱼了")
}
 

在上面的例子中,我们通过以下方式达到了面试题所需的目的:

  • 设置     runtime.GOMAXPROCS 方法模拟了单核 CPU 下只有一个 P 的场景。
  • 运行一个 Goroutine,内部跑一个 for 死循环,达到阻塞运行的目的。
  • 运行一个 Goroutine,主函数(main)本身就是一个 Main Goroutine。

思考一下:这段程序是否会输出 ”脑子进煎鱼了“ 呢,为什么

答案是:

  • 在 Go1.14 前,不会输出任何结果。
  • 在 Go1.14 及之后,能够正常输出结果。
 

为什么

这是怎么回事呢,这两种情况分别对应了什么原因和标准,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。
  • 抢占运行时间过长的 G。

该方法会检测符合场景的 P,当满足上述两个场景之一时,就会发送信号给 M。M 收到信号后将会休眠正在阻塞的 Goroutine,调用绑定的信号方法,并进行重新调度。以此来解决这个问题。

注:在 Go 语言中,sysmon 会用于检测抢占。sysmon 是 Go 的 Runtime 的系统检测器,sysmon 可进行 forcegc、netpoll、retake 等一系列骚操作(via @xiaorui)。

在这篇文章中,我们针对 ”单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?“ 这个问题进行了展开剖析。

针对不同 Go 语言版本,不同程序逻辑的表现形式都不同,但背后的基本原理都是与 Go 调度模型和抢占有关。

上述内容就是Go语言中单核CPU开两个Goroutine时其中一个死循环会怎么样,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联行业资讯频道。


本文题目:Go语言中单核CPU开两个Goroutine时其中一个死循环会怎么样
文章URL:http://scyanting.com/article/jeccdp.html