1. GMP模型
这里首先给出GMP模型的调度策略. 让大家有一个全局的认识更好
2. G (groutine)
G就是goroutine的意思, 代表了一个协程. 每次go调用的时候,都会创建一个G对象,它包括栈、指令指针以及对于调用goroutines很重要的其它信息,比如阻塞它的任何channel,其主要数据结构:
type g struct {
stack stack // 描述了真实的栈内存,包括上下界
............. // 中间还有很多部分
m *m // 当前G的绑定的m
sched gobuf // goroutine切换时,用于保存g的上下文
param unsafe.Pointer // 用于传递参数,睡眠时其他goroutine可以设置param,唤醒时该goroutine可以获取
atomicstatus uint32
stackLock uint32
goid int64 // goroutine的ID
waitsince int64 // g被阻塞的大体时间
lockedm *m // G被锁定只在这个m上运行
............. // 省略
}
看这个结构我们可以得知, G需要与M进行绑定. 当然最重要的还是sched这个结构体 存储了调度时g的上下文(context)
type gobuf struct {
// 保存CPU的rsp寄存器的值
sp uintptr
// 保存CPU的rip寄存器的值
pc uintptr
// 记录这个gobuf对象属于那个g
g guintptr
ctxt unsafe.Pointer
ret sys.Uintreg
lr uintptr
bp uintptr // for framepointer-enabled architectures
}
sp, pc 寄存器的值, 栈指针等等. 记录了上下文
3. M(machine)
M代表了一个线程 . 每次创建一个M的时候,都会有一个底层线程创建;所有的G任务,最终还是依附于M上执行,其主要数据结构:
type m struct {
g0 *g // 带有调度栈的goroutine
gsignal *g // 处理信号的goroutine
tls [6]uintptr // thread-local storage
mstartfn func()
curg *g // 当前运行的goroutine
caughtsig guintptr
p puintptr // 关联的p和执行的go代码
nextp puintptr
id int32
mallocing int32 // 状态
spinning bool // m是否out of work
blocked bool // m是否被阻塞
inwb bool // m是否在执行写屏蔽
}
其中重点要关注的是 g0 和 curg , curg代表的当前执行的groutine协程
另一个是g0,是带有调度栈的goroutine,这是一个比较特殊的goroutine。普通的goroutine的栈是在堆上分配的可增长的栈,而g0的栈是M对应的线程的栈。所有调度相关的代码,会先切换到该goroutine的栈中再执行。也就是说线程的栈也是用的g实现,而不是使用的OS的。
4. P(processor)
执行器或者处理器. 相当于一个管理者. 每一个M必须要绑定一个P. P自身有一个局部本地队列用来保存g. 并且所有的p都共享这一个全局队列
P负责调度goroutine,维护一个本地goroutine队列,M从P上获得goroutine并执行,同时还负责部分内存的管理。
下面的p的局部实现
lock mutex
id int32
status uint32 // 状态,可以为pidle/prunning/...
link puintptr
schedtick uint32 // 每调度一次加1
syscalltick uint32 // 每一次系统调用加1
sysmontick sysmontick
m muintptr // 关联的m
mcache *mcache
racectx uintptr
goidcache uint64 // goroutine的ID的缓存
goidcacheend uint64
runqhead uint32 // 头部
runqtail uint32 // 尾部
// 队列在这呢
runq [256]guintptr // 可运行的goroutine的队列
runnext guintptr // 下一个运行的g
}
5.调度顺序
下面说一下详细的过程
- go func(){}创建一个新的goroutine
- 这个goroutine会被挑选进入一个P的本地队列, 若是本地队列满了, 那么就进入全局的队列
- 这时M向P要goroutine去执行了. G依附于M上运行,每个M绑定一个P。如果P的本地队列没有G,M会从全局队列寻找G, 如果全局队列也没有G, 那么P会从其他P里面窃取G
- M被操作系统调度到CPU上面执行, G也就执行了, 若是在此期间,M这个线程遇到阻塞或者系统调用, 那么此时P就会与M解绑. 并在M的休眠队列里面唤醒一个新的M与之绑定, 然后再去运行G.
- 若是在此期间是G发生了阻塞, 那么M也会阻塞. 此时M就会与P解绑, 由M1接管P.去运行其他的G. M等待阻塞的返回, 就将G放入全局队列中了, 自己就去休眠了.
- 运行完之后G就会被销毁退出.
5.1 协程调度
5.1.1 主动调度
主动的让出CPU资源, 此时当前协程切换到g0, 取消G与M之间的绑定关系, 再将此G放入全局队列中, 并调用schedule函数开始新一轮的循环
5.1.2 抢占式调度
当某个协程占用CPU时间过长时, go语言调度器就会强制中断执行.并保存上下文等待下次调度