go 调度器实现
GO 语言的调度器
目录
- GMP 模型简介
- 调度器实现机制
GMP 模型简介
先来一张经典的GMP 关系图
img
- G 是go程序中调度器中的一个任务,类似操作系统中的线程,主要处理任务。
- M 是内核线程的一种抽象,由操作系统负责调度和管理,主要负责执行goroutine。
- P 是go程序运行期的本地调度器,提供线程的上下文环境,主要负责goroutine和线程的中介工作。
调度器的实现机制
package main
func main(){
ch := make(chan int)
go func(){
ch <- 1
}
<-ch
}
只要在方法前面加上关键字go就能很容易的开启一个goroutine, 如上 述代码所示, 是不是so easy。可他究竟是怎么调度的呢?先来看下Go 程序运行的结构,Go 程序主要两部分构成 用户层和运行层,见下图
img在Runitme 层有三个基础的结构体runtime.G, runtime.M, runtime.P, 在加上一个核心的结构体runtime.Sche 来实现goroutine的调度保证Go 程序的高效执行,下面介绍它是如何高效的调度的。
imgGo 的调度器是有两个goroutine队列 的,一个是放在P 上的Local 队列,一个是放在sche上的全局队列。早期的版本确实只有一个全局队列,但当并发比较大的时后多个M获取G时导致加锁解锁的开销比较大,后来的版本就引入了P, 在P上有一个Local队列从而减少锁的开销。当开启一个goroutine时,先把G放到P上的队列当中,等需要执行G时,P需要绑定一个M,将G运行在M上。未分配给P的Goroutine则放到全局队列当中。
每个goroutine执行的快慢不一样,会产生有的P很忙,有的P很闲。 调度器让空闲的P偷其他P上goroutine到自己的队列上,其他p上也没有则从全局队列中偷取,从而充分利用了计算机上的每一个cup高效的执行任务。(P:想偷懒都不行了 哈哈)
当一个goroutine执行一些系统任务阻塞了M挂起来怎么办呢?下面从同步和异步分析下
先分析下同步的情况,当G1 进行系统调用时,M1就被阻塞了,G1挂在M1上, M2被引入了到P上,G2继续被调度。当G1的系统调用完成以后G1又会被放入P的LRQ当中,等待被调度执行。
img
再来看下异步的情况,当G1进行网络读写时,G1的异步请求会被netpoll接手,这时候M是没有被阻塞的,G2会被继续执行。等G1的异步请求完成以后G1 又会重新放到P的LRQ当中,等待被调度执行。
img
总结
- 介绍了GMP模型,主要介绍了每个模型是什么和它的主要作用
- 为了能够更好的并发执行,探究了调度器的实现机制。这里也只是抛砖引玉,调度机制其实很复杂,goroutine的各种状态,协作式和抢占式的调度实现等,后面从源码的角度再分析它的复杂机制。