- G:goroutine,go程序建立的用户线程。主要保存 goroutine 的运行时栈信息(stack结构体)以及 CPU 的一些寄存器的值(gobuf结构体),还有关联的M,全局队列中下个G等信息。
- M:machine 一个
M直接关联一个os内核线程,用于执行G。M会优先从关联的P的本地队列中直接获取待执行的G,它保存了M自身使用的栈信息、当 前正在M上执行的 G 信息、与之绑定的P信息。当有一个M阻塞,会有一个新的M被创建;当有一个M空闲,会被回收或睡眠。- P:processor 代表了
M所需的上下文环境,也是处理用户级代码逻辑的处理器,可以看作一个局部调度器使go代码在一个线程上跑。在创建程序的时候创建一个P列表, 最多有$GOMAXPROCS个,这环境变量可以通过操作系统中的环境变量设置,也可以通过Go程序中的runtime.GOMAXPROCS()函数设置,默认为处理器的核心数,它代表了真正的并发度。- P的本地队列:
P维护一个runq_用来存放等待执行的goroutine,新创建的G会优先放在本地队列,当本地队列满(256G)时,会放入G的全局队列。- 全局队列:如果
P的本地队列已满,待执行的G就会放在全局队列中,M会先从关联的P的本地队列中获取待执行的G,没有的话,再到全局队列中获取;如果这里也没有了,就去其他P的本地队列中获取一些任务。
上面是一些摘抄,记录一下自己的理解,M在GMP模型中代表着实际的线程,系统可能给一个go程序分配多个线程,同时线程的管理也是动态的,不够就新建,多了就收回。G是用户创建的协程,被管理的对象们。P是GMP调度模型的核心,他有几层含义:第一,协程都需要挂靠到某一个P上进行调度,P的个数就是可以同时运行的协程G的个数,所以P代表着某时刻实际的并发度,P的数量默认也是系统的核数;第二,每个P都有一个本地队列,挂载一个队列的协程,协程们需要通过这个P的调度,最终在一个M中获得执行。
有天晚上想到了一个直观的比喻,但是也不知道合理不合理,不合理以后再改。M就是枪(没有弹夹的那种),P是弹夹,G是子弹。
协程调度目标是让所有协程获得执行,类比枪支就是最后将所有的子弹击发出去。所有子弹都需要装载到一个弹夹中,才能获得击发(许多个G需要依赖一个P);弹夹需要获得一把枪,才能击发子弹(M和P的关系);弹夹数量相对固定,需要根据弹夹数量动态调整枪的数量,弹夹的数量决定了能同时击发的子弹的数量(P代表实际并发度);如果一个子弹在一个枪中卡壳了,可以把弹夹拆掉换到另一把枪上(P队列的调度)。
Go routine从创建到执行的过程
- 协程创建,如果本地队列有空间,则存放到本地队列,如果本地队列已满,就放到全局队列中。
- M从绑定的P本地队列中获取一个可执行的G,如果本地队列为空,则从全局队列中获取(周期性的也会看一下全局队列,防止其中的协程饿死);全局队列也为空,就从其他P的本地队列中“窃取”一半的协程进行执行。
- M在被一个协程阻塞时,会主动解除和P的绑定,使得其他协程也有获得执行的机会
go的1.14版本中,go语言的技术团队尝试在调度器中添加了可抢占的技术。
- 抢占技术的出现一方面解决了线程M在执行计算密集型任务时长时间占用CPU,导致与之绑定的P上的其他G得不到执行而造成的"饥饿现象";
- 另一方面,抢占技术的出现对GC来讲解决GC时可能出现的deadLock
回头有需要再看源码
其他一些参考文档:zhuanlan.zhihu.com/p/261590663