Go调度相关
go 调度
go routinue在线程中进行调度
GPM的概念:
- G(Goroutine): 即Go协程,每个go关键字都会创建一个协程。
- M(Machine): 工作线程,在Go中称为Machine。
- P(Processor): 逻辑处理器(Go中定义的一个摡念,不是指CPU),包含运行Go代码的必要资源,也有调度goroutine的能力。
GPM的关系:
1.M必须拥有P才可以执行G中的代码,交给线程进行执行。默认情况下等同于CPU的核数。一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个
2.P含有一个包含多个G的队列,调度G交由M执行。默认情况下等同于CPU的核数
3.P可以调度G交由M执行
调度算法
image-
G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。
-
P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。
-
M: Machine,OS 线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。
-
每个 P 维护一个 G 的本地队列;
-
当一个 G 被创建出来,或者变为可执行状态时,就把他放到 P 的本地可执行队列中,如果满了则放入Global;
-
当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, M 就随机选择另外一个 P,从其可执行的 G 队列中取走一半。
调度过程
当通过 go 关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 goroutine 并执行。执行调度算法:当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P 也不会就这么在那划水啥都不干,它会先尝试从 Global 队列寻找 G 来执行,如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。
G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:
1.如果有空闲的P,则获取一个P,继续执行G0。
2.如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。
调度策略:
1.队列轮转
2.系统调用
3.工作量窃取
go routine vs thread
内存占用:
1.goroutine 的栈内存消耗为 2 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容,最大可达 1GB。(64 位机器最大是 1G,32 位机器最大是 256M)
2.thread 则需要消耗 1 MB 栈内存,而且还需要一个被称为 “a guard page” 的区域用于和其他 thread 的栈空间进行隔离
创建和销毀:
1.goroutine 直接从runtime申请,用户级
2.thread 需要进行系统调用
切换:
1.goroutines 切换只需保存三个寄存器:Program Counter, Stack Pointer and BP。
2.threads 切换时,需要保存各种寄存器
因此goroutines切换速度比thread快很多
为什么要 scheduler:
根据scheduler的功能来说明:
- 因为所有的ggoroutines 都通过runtime维护,而众多的goroutines需要依赖thread才能执行,因此使用scheduler来对goroutines的运行进行调度控制已达到高效的执行效率。
- 若没有scheduler,比如1:1模型,在同一时刻,一个线程上只能跑一个 goroutine,当 goroutine 发生阻塞,其他goroutine就会被阻塞,大大影响了性能,而有了scheduler,被阻塞的线程会被调度走,让其他的goroutine来执行。
- 如果非要杠,没有scheduler也能运行,但是无法达到高效的目的。
Go scheduler的作用
Go scheduler 的职责就是将所有处于 runnable 的 goroutines 均匀分布到在 P 上运行的 M。
Go scheduler 每一轮调度要做的工作就是找到处于 runnable 的 goroutines,并执行它
M:N模型 和工作窃取
在任一时刻,M 个 goroutines(G) 要分配到 N 个内核线程(M),这些 M 跑在个数最多为 GOMAXPROCS 的逻辑处理器(P)上。每个 M 必须依附于一个 P,每个 P 在同一时刻只能运行一个 M。如果 P 上的 M 阻塞了,那它就需要其他的 M 来运行 P 的 LRQ 里的 。当 P2 上的一个 G 执行结束,它就会去 LRQ 获取下一个 G 来执行。如果 LRQ 已经空了,就是说本地可运行队列已经没有 G 需要执行,并且这时 GRQ 也没有 G 了。这时,P2 会随机选择一个 P(称为 P1),P2 会从 P1 的 LRQ “偷”过来一半的 G。
GPM是什么
G、P、M 是 Go 调度器的三个核心组件
go初始化流程:
1.call osinit。初始化系统核心数。
2.call schedinit。初始化调度器。
3.make & queue new G。创建新的 goroutine。
4.call runtime·mstart。调用 mstart,启动调度。
5.The new G calls runtime·main。在新的 goroutine 上运行 runtime.main 函数。
scheduler初始化过程:
1.调整 SP
2.初始化 g0 栈
3.主线程绑定 m0
4.初始化 m0
5.初始化 allp
main goroutine是如何诞生的?
main goroutine执行流程:
1.创建后台监控线程sysmon
2.runtime包初始化
3.main包初始化
4.执行用户main函数
5.调用exit(0)退出
普通 goroutine执行流程:
先是跳转到提前设置好的 goexit 函数的第二条指令,然后调用 runtime.goexit1,接着调用 mcall(goexit0),而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。到这里,普通 goroutine 才算完成使命
g0栈和用户栈如何切换?
这中间涉及到栈和寄存器的切换
如何保存g0的调度信息?
schedule函数有什么重要作用?
gogo函数如何完成从g0到main goroutine的切换?
schedule如何循环运转?
schedule() -> execute() -> gogo() -> goroutine 任务 -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule()
g0 栈的作用就是为运行 runtime 代码提供一个“环境”。
M如何找工作
1.先从本地队列找
2.定期会从全局队列找
3.最后实在没办法,就去别的 P 偷
sysmon后台监控线程作用(执行监控任务):
1.抢占处于系统调用的 P,让其他 m 接管它,以运行其他的 goroutine
2.将运行时间过长的 goroutine 调度出去,给其他 goroutine 运行的机会
sysmon函数的作用
sysmon 执行一个无限循环,一开始每次循环休眠 20us,之后(1 ms 后)每次休眠时间倍增,最终每一轮都会休眠 10ms;
sysmon 中会进行 netpool(获取 fd 事件)、retake(抢占)、forcegc(按时间强制执行 gc),scavenge heap(释放自由列表中多余的项减少内存占用)等处理