Go调度相关

2021-06-10  本文已影响0人  夜空中乄最亮的星

go 调度
go routinue在线程中进行调度

GPM的概念:

GPM的关系:

1.M必须拥有P才可以执行G中的代码,交给线程进行执行。默认情况下等同于CPU的核数。一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个
2.P含有一个包含多个G的队列,调度G交由M执行。默认情况下等同于CPU的核数
3.P可以调度G交由M执行

调度算法

image

调度过程

当通过 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的功能来说明:

  1. 因为所有的ggoroutines 都通过runtime维护,而众多的goroutines需要依赖thread才能执行,因此使用scheduler来对goroutines的运行进行调度控制已达到高效的执行效率。
  2. 若没有scheduler,比如1:1模型,在同一时刻,一个线程上只能跑一个 goroutine,当 goroutine 发生阻塞,其他goroutine就会被阻塞,大大影响了性能,而有了scheduler,被阻塞的线程会被调度走,让其他的goroutine来执行。
  3. 如果非要杠,没有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(释放自由列表中多余的项减少内存占用)等处理

上一篇下一篇

猜你喜欢

热点阅读