介绍
在该系列的第一篇文章中,我们已经解释了操作系统调度的各个方面,我认为这对理解和欣赏Go调度的语义非常重要。在这一部分,我将从语义层面讲一下Go的调度是如何进行的。Go调度调度本身其实很复杂,很多细节不用过分关注,重要的是通过一个模型来理解Go的调度机制,这有助于在实践中做出更好的决策。
辅助知识——物理核、逻辑核
CPU( CentralProcessingUnit): 中央处理单元,CPU不等于物理核,更不等于逻辑核。
物理核(physical core/processor): 可以看的到的,真实的cpu核,有独立的电路元件以及L1,L2缓存,可以独立地执行指令。
逻辑核( logical core/processor,LCPU): 在同一个物理核内,逻辑层面的核。(比喻,像动画片一样,我们看到的“动画”,其实是一帧一帧静态的画面,24帧/s连起来就骗过了人类的眼睛,看起来像动起来一样。逻辑核也一样,物理核通过高速运算,让应用程序以为有两个cpu在运算)。
超线程( Hyper-threading, HT):超线程可以在一个逻辑核等待指令执行的间隔(等待从cache或内存中获取下一条指令),把时间片分配到另一个逻辑核。高速在这两个逻辑核之间切换,让应用程序感知不到这个间隔,误认为自己是独占了一个核。
关系: 一个CPU可以有多个物理核。如果开启了超线程,一个物理核可以分成n个逻辑核,n为超线程的数量。
参考:https://cloud.tencent.com/developer/article/1465603
程序启动
Go启动后会为其分配一个逻辑处理器 (Logical Processor,P)。对于一台机器而言,逻辑处理器的数量是由机器的虚拟核心决定的,有几个虚拟核心就有几个逻辑处理器。而虚拟核心数又和CPU的架构有关的。比如下面这个机器的硬件配置,显示有1个处理器,6个物理核心。理论上一个物理核只会有一个物理线程,但是由于Inter Core i7 的CPU有超线程技术,也就是说一个物理核可以有2个物理线程。所以这个机器实际上有12个物理线程,也可以认为是有12个虚拟核心,也就是说有12个逻辑处理器。
这个值可以通过Go的runtime
来验证:
Listing 1
package main
import (
"fmt"
"runtime"
)
func main() {
// NumCPU returns the number of logical
// CPUs usable by the current process.
fmt.Println(runtime.NumCPU()) // 12
}
当我在自己的本地机器上运行上面这个程序的时候,NumCPU()
的输出结果是12,这意味着在我机器上跑的任何Go应用程序都会分配12个P。
而每个P都会赋予一个操作系统线程(M),M代表机器的意思。这个线程仍然由操作系统管理,操作系统负责将其放到Core上来执行——正如我们在上一篇文章所讲的那样。这也就意味着,我在我的本机上跑Go应用程序,那将会有12个线程来处理所有的工作,每个线程被赋给了一个P。
而每一个Go应用程序都有一个初始的Go协程,其实就是Go应用程序的执行路径。Go协程本质上就是协程( Coroutine ),但是因为这里是Go所以把协程的第一个字母C换成了G,所以就有了Goroutine这么个词。你可以把Go协程认为是应用程序级别的线程,和操作系统线程不一样但很多方面很类似。区别在于,操作系统线程是在Core上切换上下文,而Go协程是在M上切换上下文。
最后一个难点是运行队列。Go调度程序中有两个不同的运行队列:全局运行队列(GRQ,Global Run Queue)和 局部运行队列(LRQ,Local Run Queue)。每个P都有一个LRQ,该LRQ管理分配给在P上下文中执行的Goroutine。这些Goroutine轮流在