简析goroutine调度原理
相较于操作系统线程,goroutine的资源占用和使用代价要小得多。可以创建成百上万甚至上千万个goroutine。Go运行时负责对goroutine进行调度惯例,即决定何时哪个goroutine获得资源执行,哪个goroutine停止执行让出资源,哪个goroutine被唤醒恢复执行等。
goroutine是Go语言并发的基础,也是最基本的执行单元。Go基于goroutine建立了GPM调度模型。
G:goroutine在go运行时中的抽象模型。代表goroutine,存储了goroutine的执行栈信息、goroutine状态及goroutine函数信息,G对象是可以重复使用的。
P:G和M之间的一个中间层。P的数量决定了系统内最大可并行的G数量(P数量 < CPU核数)
M: 对操作系统线程(被视为”物理CPU“)的一种抽象,代表这真正的计算资源
G想要运行起来,首先需要分配一个P,即进入P的本地运行队列中(local runq),P又和M绑定,P和M绑定后P的本地队列中的G才能运行起来。P和M的关系类似Linux操作系统调度层面用户线程与内核线程的对应关系:多对多。
Go调度器实现了抢占式调度,调度到死循环后不会一直执行下去,即G不会一直占用分配给它的P和M,位于P的本地队列中的其他G可以得到调度。
import ( "fmt" "time" ) func endlessLoop1() { for { time.Sleep(1 * time.Second) fmt.Println("I am endlessLoop1") } } func endlessLoop2() { for { time.Sleep(1 * time.Second) fmt.Println("I am endlessLoop2") } } func main() { runtime.GOMAXPROCS(1) // 设置P的数量,此处为1,即多个goroutine都加入一个P的本地队列中等待被调度 go endlessLoop1() go endlessLoop2() for { time.Sleep(1 * time.Second) fmt.Println("I can be arrived!") } }
上边代码中启动了三个goroutine:一个main goroutine,其中for循环为死循环。两个endlessLoop goroutine,endlessLoop也是一个死循环。下面运行程序
go run go-preempt-scheduler.go I can be arrived! I am endlessLoop1 I am endlessLoop2 I can be arrived! I am endlessLoop1 ...
从运行结果可以看到,三个在一个P的队列中的死循环goroutine都得到了调度。
Go语言学习笔记、语法知识、技术要点和个人理解及实战