说起go语言,离不开goroutine。
之前使用go语言开发的时候,也没多少机会用到goroutine。
趁这些天了解一下GMP模型G(goroutine) M(thread) P(Processor)。
1.GMP模型
G -> goroutine
Go中,协程被称为goroutine,一个goroutine只占几KB。
而且调度也很灵活(是通过runtime调度的)。
P -> Processor
它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。
M -> thread
每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。
协程是程序级别的,
协程是通过runtime调度器的Processor分配到thread。
线程thread是操作系统级别的。
thread是通过操作系统OS调度器分配到CPU上执行内容。
看看下图:
我们从下往上看每个节点的作用:
CPU:=> 线程是在CPU中执行。每个CPU一次只能处理一个线程thread。
M(thread):=> M从P本地队列中获取G执行,执行后从P获取下一个G, 不断的重复下去。
OS调度器:=> 管理M和CPU之间的调度。
P(Processor):=> P是M与G之间的协调。所有的P都在程序启动时创建,最多有GOMAXPROCS(可配置)个。P和M是1:1的关系。
goroutine调度器:=> 管理P和G之间的调度。
P的本地队列:=> 存放等待运行的G, 新建G时会优先加入到P的本地队列, 如果队列满了, 则会把本地队列中一半的G移动到全局队列。
全局队列(Global Queue):=> 存放等待运行的G。
2.Go调度器调度过程解析
下面分析一下调度器调度的过程。
◆func()调度过程
假设有一个 go func(), 简单的模拟一下调度的过程。
- => go func()
- => 创建G
- => G进入P的本地队列(满了进入全局队列)
- => 和P有关联的M通过P获取G(如空的话去别的本地列或全局队列里获取)
- => M在CPU中执行。
◆main()调度过程
看下一段代码执行的过程
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
- => runtime 创建最初的线程 M0 和 G0,并把2者关联。
- => 调度器初始化:初始化 M0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS设定个数的P和P列表。
- => 代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,然后把main goroutine加入到P的本地队列。
- => 上述操作中,M0已经绑定了P0,会从P的本地队列获取G,获取到 main goroutine。
- => M根据G中的栈信息和调度信息设置运行环境后, M运行G。
- => 执行完G后退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,
runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。
runtime.main的goroutine 运行是调度器的真正开始,直到runtime.main结束。
这里补充说明一下M0和G0
M0 是启动程序后的编号为0的主线程,M0对应的实例会在全局变量 runtime.m0 中,不需要在heap上分配,
M0 负责执行初始化操作和启动第一个 G, 在之后M0就和其他的 M 一样了。
G0 是每次启动一个M都会第一个创建的 gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数,每个M都会有一个自己的G0。
在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0。
◆深入解剖调度过程
看下图:
刚开始的时候G0会协调要执行的G协程,执行G1的时候有又需要执行G2,这时候优先会放到自己的P本地队列,
G1执行完后,G0会依次协调执行G2。
执行的过程中M如果阻塞了,那P会寻找下一个可执行的M,继续执行下一个G。
有2个以上P的时候:
P2里的本地队列已经没有可执行G了,他就从全局队列中获取可执行G。
全局队列里没有可执行G时,会从别的P本地队列中偷取可执行G来执行。
没有可执行G就会进入自选状态等待下一个G。
3.结语
了解这些底层逻辑是为了更好的时候这些功能,
了解这些goroutine轻便的调度功能,后续写代码的时候多多少少会有一些帮助吧?
欢迎大家的意见和交流
email: li_mingxie@163.com