Go并发调度进阶-循环调度,不是你理解的死循环
Go并发调度进阶-【公粽号:堆栈future】
原文
3. 循环调度
所有的GMP初始化工作都已经完成了,是时候启动运行时调度器了。我们已经知道,当所有准备工作都完成后, 最后一个开始执行的调用就是runtime.mstart了。
mstart
主要功能:
-
确定执行栈的边界
-
启动
mstart1
-
设置退出线程标记osStack=true
-
调用mexit(osStack)退出线程
再来看下mstart1
:
-
如果当前m并非m0,那么要求绑定p
-
开始调用
schedule()
开始调度
所以我们可以看到调度循环schedule
无法返回,因此最后一个mexit目前还不会被执行,因此当下所有的Go程序创建的线程都无法被释放。
1. M与P的绑定
M 与 P 的绑定过程(acquirep
函数调用wirep
绑定)只是简单的将 P 链表中的 P ,保存到 M 中的 P 指针上。绑定前,P 的状态一定是 _Pidl
e,绑定后 P 的状态一定为 _Prunning。
// 将 p 绑定到 m,p 和 m 互相引用
_g_.m.p.set(_p_) // *_g_.m.p = _p_
_p_.m.set(_g_.m) // *_p_.m = _g_.m
// 修改 p 的状态
_p_.status = _Prunning
2. M的park和unpark
无论出于什么原因,当 M 需要被park
时,可能会执行stopm
调用。此调用会将 M 进行park
,并阻塞到它被unpark
时。这一过程就是工作线程的暂止(park)
和复始(unpark)
。
func stopm() {
_g_ := getg()
(...)
// 将 m 放回到 空闲列表中,因为我们马上就要park了
lock(&sched.lock)
mput(_g_.m)
unlock(&sched.lock)
// park当前的 M,在此阻塞,直到被唤醒
notesleep(&_g_.m.park)
// 清除暂止的 note
noteclear(&_g_.m.park)
// 此时已经被unpark,说明有任务要执行
// 立即 acquire P
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
它的流程也非常简单,将 m 放回至空闲列表中,而后使用 note 注册一个暂止通知, 阻塞到它重新被复始。
3. 核心调度schedule
// 调度器的一轮:找到 runnable Goroutine 并进行执行且永不返回
func schedule() {
_g_ := getg()
(...)
top:
if sched.gcwaiting != 0 {
// 如果需要 GC,不再进行调度
gcstopm() //park 还有就是偷取没有的时候也会park
goto top
}
var gp *g
(...)
if gp == nil {
// 说明不在 GC
// 每调度 61 次,就检查一次全局队列,保证公平性
// 否则两个 Goroutine 可以通过互相 respawn 一直占领本地的 runqueue
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
// 从全局队列中偷 g
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
if gp == nil {
// 说明不在 gc
// 两种情况:
// 1. 普通取
// 2. 全局队列中偷不到的取
// 从本地队列中取
gp, inheritTime = runqget(_g_.m.p.ptr())
(...)
}
if gp == nil {
// 如果偷都偷不到,则休眠,在此阻塞
gp, inheritTime = findrunnable()
}
// 这个时候一定取到 g 了
if _g_.m.spinning {
// 如果 m 是自旋状态,则
// 1. 从自旋到非自旋
// 2. 在没有自旋状态的 m 的情况下,再多创建一个新的自旋状态的 m
resetspinning()
}
if gp.lockedm != 0 {
// 如果 g 需要 lock 到 m 上,则会将当前的 p
// 给这个要 lock 的 g
// 然后阻塞等待一个新的 p
startlockedm(gp)
goto top
}
// 开始执行
execute(gp, inheritTime)
}
看下execute
:
func execute(gp *g, inheritTime bool) {
_g_ := getg()
// 将 g 正式切换为 _Grunning 状态
casgstatus(gp, _Grunnable, _Grunning)
// M 与 G 绑定开始执行
_g_.m.curg = gp
gp.m = _g_.m
// G终于开始执行了
gogo(&gp.sched)
}
当开始执行 execute 后,g 会被切换到 _Grunning 状态,同时将 m 和 g 进行绑定。最终调用 gogo 开始执行。
gogo的执行是一段汇编代码,晦涩难懂哈
。但是大致意思就是完成g0到gp的栈切换,然后开始执行runtime.main
函数或者用户自定义的goroutine任务。
执行完成后,main goroutine 直接调用 eixt(0)
退出,普通 goroutine 则调用 goexit -> goexit1 -> mcall
,完成普通 goroutine 退出后的清理工作,然后切换到 g0 栈,调用 goexit0
函数,将普通 goroutine 添加到缓存池中,再调用 schedule 函数进行新一轮的调度。
调度太复杂了,大致流程如下:
schedule() -> execute() -> gogo() -> goroutine 任务 -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule()
可以看出,一轮调度从调用 schedule 函数开始,经过一系列过程再次调用 schedule 函数来进行新一轮的调度,从一轮调度到新一轮调度的过程称之为一个调度循环。
这里说的调度循环是指某一个工作线程的调度循环,而同一个Go 程序中存在多个工作线程,每个工作线程都在进行着自己的调度循环。
4. 小结
由于调度核心太过于复杂,大家只需要了解大致流程或者思路就OK,没必要深入底层的细节。因为陷得越深你就越难理解一些细枝末节,我写到这里放弃了,不想深入研究下去了,只需要知道这个循环调度不是一个死循环就行了,而且调度链中的各个环节也应该大致了解下。好了今天就先到这里,欢迎大家关注转发分享哈。
公粽号:堆栈future
使很多处于迷茫阶段的coder能从这里找到光明,堆栈创世,功在当代,利在千秋