以下为本人阅读goroutine调度源码随手记的笔记,现在还是一个个知识点的形式,暂时还没整理,先发到这里,一点点更新:
1). runq [256]guintptr P 的runable队列最大只能保存256个G
2). 全局的可执行队列由schedt持有,runq gQueue
3). goroutine 状态从_Grunning转为_Grunnable后,解除g与m的关联,将g放入全局的任务队列,然后再找到一个新的_Grunable状态的G来执行
func goschedImpl(gp *g) {
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock) schedule()
}
4). 寻找下一个g来执行的函数 findrunnable 在proc.go:2178
大致分为三大步骤,一是尝试从本地队列获取一个,二是尝试从全局队列获取n个,三是尝试窃取。这里要说一下的是:从本地队列获取时和从全局队列获取时关于并发访问的控制是不一样的,从本地队列获取的时候,都是通过原子操作来和自旋锁来实现的,而从全局队列获取时,则是通过获取sched.lock锁控制的,这是一个全局的排他锁。
首先尝试从本地队列获取(本地队列先看runnext是否不为0,如果是,先返回runnext指向的g,否则从队首弹一个g出来),如果本地队列没有则从全局队列获取,如果全局队列也没有,就尝试从其他P的队列中窃取,但是窃取之前会先做如下几个判断:
1. sched.npidle == gomaxprocs - 1 也就是看其他的P是不是都空闲着,如果是的话,就没必要去窃取了。
2. 2*sched.nmspinning >= gomaxprocs - npidle 当前处于spinning状态的M的两倍 大于等于非空闲的P的数量. 翻译过来就是在正在窃取G的M的数量应该不超过非空闲状态的P的一半
3. 是否准备执行GC,如果是,就先不窃取了
5).从全局队列取一部分g到本地队列的操作。
源码位置: proc.go:4665 globrunqget (go1.12)
关于取多少个的问题。假设全局队列一共有t个G,一共有n个P,则,取的数量不超过t / n,并且不超过本队队列总长度的一半,目前1.14版本中,本地队列总长度为256,也就是每次从全局队列取的G的数量不超过128个.
6).关于选取哪个P进行窃取以及窃取多少个的问题:
源码位置:proc.go:2238 findrunnable(go1.12)
1. 选取窃取的P
首先一共进行4轮尝试,每轮都会遍历所有的P。
另外这里的遍历P的过程值得说一下,并不是从下标0位置一个个往后遍历的,而且跳跃式的遍历,而且跳跃的步长是随机出来的。关于步长的选取,算法是这样的:
首先定义了一个randomOrder结构如下:
type randomOrder struct {
count uint32 // P的数量
coprimes []uint32 // 小于等于count,且与count互质的数(从小到大排列)
}
首先会生成一个与P的数量互质的数的列表,然后随机出一个数i,遍历P列表的步长= i % len(coprimes)。开始遍历的位置=i % nprocs (nproc即P的数量,源码中是用i对count取模,而count的值是通过randomOrder.reset操作把nprocs赋值给count的).
有意思的是,当第一轮遍历完了之后,发现一个g都没拿到,这个时候就会把stealRunNextG的值置为true,这个值表示是否从别的P的runnext窃取,第一轮窃取的时候这个值都为false。(也就是饥不择食了)
7). 窃取数量
源码位置: runqgrab proc.go:4858
假设现在P1从P2窃取一部分G。关于窃取多少个G是怎么定的呢?有以下几点原则,第一,假设P2的可执行队列中G的数量为x个,窃取个数n = x - x / 2;第二不得超过P1的可执行队列(环形buffer)容量的一半,当前最新版本(go1.14)中P的可执行队列最大容量为256,因此,窃取G的个数不超过128个。
8). 最终没有找到可执行的G之后发生了什么
1. 断开m和p之间的联系
2. 把P放进sched.pidle(空闲P的链表)
3. 把M的spinning(是否正在获取可用G)状态改为false
4. 再次确认一遍所有的P的runq都是空的
5. 尝试轮训阻塞事件列表,看看是否有一些G已经转变为runable状态.此处获取到的gList将尝试空闲的P列表中取一个P出来,然后把gList放进P的runq。而如果没有空闲的P,那么将把gList放进全局的runq。
6. 若步骤5仍然没能获取到可执行的G,或者步骤5获取到了G,但是没有空闲的P,那么将这部分G放到全局队列后,M将停止运行,将当前M加入到空闲M列表,然后开始睡眠,直到下一次被唤醒。
9).全局队列的来源有以下几种:
1. 轮询等待队列,解除等待之后将放入全局队列
2. 在M上正常执行的g,被切换之后被放入全局队列
3. 插入新的goroutine时,如果本地队列已满,则会将本地队列的一半取出来,再加上新加入的g,一起放入全局队列,放入全局队列前,会将取出来的这个列表随机打乱顺序再放入proc.go:4803(go1.12)
10).一个go程序在64位机上,最小的栈空间大小是2048个字节,最大的栈空间大小是1G,在32位机上是250M,不过这里要注意,这里的1GB并不等于1024M,而是10的9次方字节,proc.go中有这样一句话:Using decimal instead of binary GB and MB because they look nicer in the stack overflow failure message.因为看起来更好看。
11).go关键字新建一个协程时,传的参数大小是有限制的,最大是2048个字节,实际上比这个还要小一点(经测试,参数最大为2000是没问题的,2001个字节的时候,会抛出这个错误:newproc: function arguments too large for new goroutine.
12).新建G时,不会总是重新分配,而是先尝试从本地空闲的G 列表里面取一个,如果本地空闲G列表没有,则从全局空闲G列表中取,如果都没取到,就重新分配一个。
13).关于新创建的G会放到哪个P的队列的问题
以下是runqput函数的注释,在创建新的G之后,传入的next始终是true的,所以,新创建的G会放到runnext,因此,新创建的G会在当前的G被换出后,立即执行,不用排队。
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the _p_.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
14). sudog用来保存g的等待队列,例如channel的发送或接收等待队列
type sudog struct {
g *g
isSelect bool
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
acquiretime int64
releasetime int64
ticket uint32
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}