GO调度器模型
GO的调度器可以充分利用多核心CPU,任何时候都有M个go协程在N个系统线程上进行调度, 这些线程在最多 GOMAXPROCS 个CPU核心上运行,这种调度模型称之为GMP模型:
如下图,每个P都有一个本地队列存放待运行的G,另外有一个全局队列,每个M需要依附在P上运行,一个P可以对应多个M, 但是同一时间一个P上只会有一个正在运行的M。
每轮调度只需要找一个可运行的G执行它就行,这个查找过程如下:
runtime.schedule() {
// 1/61 的概率去全局队列找一个G来运行
// 如果没有找到,到本地队列中找
// 如果没有找到,
// 尝试从其他的P中偷取G来运行
// 如果没有,检查全局队列
// 如果没有,检查 Net Poller
}
GO协程和线程一样有三种状态,一个协程可以处于下面三种状态中的一种: Waiting , Runnable 和 Executing 。
GO程序中的下列4类事件可以触发调度器执行调度任务。并不是说这些事件发生时调度器一定会执行调度,只是说此时调度器有机会执行调度:
大多数操作系统都支持网络轮询,例如MacOS的kqueue,linux的epoll接口。Go会利用网络轮询接口来异步处理网络请求, 当G调用网络系统调用时调度器会将此G调度出去以避免M被阻塞,然后调度队列中的其他G继续执行,因此不需要创建一个新的M,减少调度开销。
在图1中,Goroutine-1正在M上运行,此时本地队列中有3个G在等待运行,网络轮询器上是空的。
图2中,Goroutine-1希望进行网络系统调用,此时将Goroutine-1移至网络轮询器上处理异步网络系统调用然后将Goroutine-2调度到M上继续运行。
图3中,网络调用完成,此时Goroutine-1被放回本地队列中,当调度到Goroutine-1时,它可以继续执行接下来的指令,这里最大的好处是执行网络系统调用不需要额外的M, 网络轮询器实际是一个系统线程专门用于处理异步网络请求。
当G调用同步系统调用时会怎样?例如文件相关的系统调用以及使用CGO时调用C函数也是同步调用,此时M会被此G阻塞。
图4中,Goroutine-1正在M1上运行,他要执行同步系统调用,此时会阻塞M1。
图5中,调度器可以探测出M1被Goroutine-1阻塞了,此时调度器会将M1和P分开,但是Goroutine-1还是在M1上。 然后搞一个M2继续在P上运行,此时可以调度Goroutine-2继续执行。GO会维护一个线程池只有线程池中没有M才会创建新的M,所以这种M切换是非常快的。
图6中,Goroutine-1的同步阻塞调用完成,此时Goroutine-1会被转移到P的本地队列中待被调度执行,此时M1会被放在线程池待下次使用。
任务窃取作用是平衡P之间的负载,如果某个P上的G都执行完了,此时会检查其他P上有没有可执行的G,如果有则会窃取其他P上的G来执行。
图7中,有两个P,每个P上有4个G,全局队列中也有一个G。
图8中,P1上的G全部执行完了,但是P2和全局队列上还有G待执行。此时P1需要窃取其他G来执行,窃取规则和调度规则是一样的参考上面的 runtime.schedule 。
图9中,根据窃取规则,P1会将P2上一半的G窃取过来执行。
图10中,如果此时P2上的G都执行完了,并且P1的本地队列中也没有G了会怎么办?
图11中,P2上的G都执行完,它要开始窃取任务,但是P1上也没有G了,根据窃取规则他会把全局队列上的G拿过来执行。
这篇文章基本上是翻译下面的文章,然后加了一些自己的理解。