G:goroutine协程。
P:processor处理器。起承上启下的作用,对于G来说是处理器,对于M来说用于屏蔽对G的调度,内存控制等逻辑。
M:thread线程。
目录
有关 P 和 M 个数和创建问题
P和M都可以自定义设置。go语言启动时,会自动默认10000,但是内核很难支持这么多线程数。
并且P和M并不是一对一的关系,一个M阻塞,P可以创建或者切换到另一个M。
P何时创建:在确定P的最大数量N后,运行时会创建N个P
M何时创建:没有足够的M来关联P并运行可运行的G的时候(比如所有M都阻塞,但还有很多P就绪任务)就会创建新的M。
调度器的设计策略
为了避免平凡创建和销毁线程,则会对线程进行复用。
1)work stealing 机制
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
2)hand off 机制
当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
3)抢占策略
在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死
4)全局G队列
在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
为了不让全局队列的G饿死,GO语言调度器执行策略:P每执行61次调度,就优先从全局队列取一个G到P中,并执行下一个执行的G。
我们以下图来看看一个go func () 调度流程
1,go func()创建一个goroutine,有两个存储G的队列,一个P的本地队列,一个全家G队列,新创建的G会先保存在P的本地队列中,若P的本地队列已满了则存放全局。
2,若一个P的本地队列为空,则优先从别的MP队列取出G来执行
3,当M调度的某一个G系统调用阻塞或其他阻塞,则runtime将会将这个M和P解除,然后创建一个新的M和这个P组合。
4,当M系统调用结束或者阻塞结束,这个G会尝试取一个空闲的P执行,并放入P的本地队列,若取不到P,则这个M将休眠放入空闲线程中,然后G放入全局队列。
5,G的调度具有亲和性。
调度器的生命周期
其中这里有特殊的M0和G0。
M0:启动程序后的0号主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。
G0:每次启动一个M都会创建第一个Goroutine,G0用于调度G,G0不指向任意函数,每一个M都有自己的G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。例如:现在本地队列上有G1 和G2,G1 运行完成后 ,M 上运行的 goroutine 切换为 G0,G0 负责调度时协程的切换(函数:schedule)。从 P 的本地队列取 G2,从 G0 切换到 G2,并开始运行 G2 (函数:execute)。实现了线程 M1 的复用。
以下面这个代码为例子:
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
- 1.runtime 创建最初的线程 m0 和 goroutine g0,并把 2 者关联。
- 2.调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS 个 P 构成的 P 列表。
- 3.示例代码中的 main 函数是 main.main,runtime 中也有 1 个 main 函数 ——runtime.main,代码经过编译后,runtime.main 会调用 main.main,程序启动时会为 runtime.main 创建 goroutine,称它为 main goroutine 吧,然后把 main goroutine 加入到 P 的本地队列。
- 4.启动 m0,m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine。
- 5.G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境
- 6.M 运行 G
- 7.G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。
11大场景和参考文献
P调度策略
type p struct{
runq [256]guintptr //局部队列。用队头队尾指针模拟循环队列
runnext guinptr //下一个执行的G
}
调度策略: runnext -> p.runq -> schet.runq(全局队列) -> 其他P剽窃(随机遍历所有的P,将有G的局部队列剽窃一半到自己队列中) -> 未找到G休眠。因为存在从其他P剽窃行为,所以访问时要加锁。