简介
G 是调度器中待执行的任务,在运行时用 runtime.g 结构体表示(内有_defer字段表示defer链表)。他可以分为三种状态(等待中、可运行、运行中(状态非常多))。结构体中也有m指针字段,跟m进行动态绑定。
M 是操作系统线程,调度器最多可以创建10000个线程,但是最多会有GOMAXPROC(默认当前机器的核数)个线程能够正常运行(可以手动改变)。在go中使用runtime.m表示。他有一个 g0 字段和一个 curg 字段,g0是持有调度栈的goroutine,curg是在当前线程上运行的用户的goroutine。g0他会深度的参与到运行时的调度过程(goroutine的创建、大内存分配)。还有关于处理器 P 的字段
P 是调度器中的处理器,是线程和goroutine的中间层,他可以提供线程需要的上下文,也会负责调度线程上的等待队列。还有处理器的数量是跟线程数相等的。然后在运行时go中使用runtime.p结构体来表示。他存储了处理器持有的运行队列、等待执行的goroutine列表、下个需要执行的goroutine等信息。P中最多存储 256 个g。
g的状态
刚开始创建时为_Gidle状态,初始化完成之后为_Gdead状态。当环境都准备就绪后会进入_Grunnable就绪态,当被调度器调度到时进入_Grunning运行态,当运行时代码中越过用户态进入内核态发生一些系统调用时会进入_Gsyscall状态,当执行完成之后会重新进入_Grunnable就绪态,等待被重新调度。当在用户态视角下发生一些阻塞时(加锁时的阻塞,channel的阻塞)会进入_Gwaiting状态,当某些条件达成之后会被切换成_Grunnable就绪态。当正常的调度完成之后会进入_Gdead被销毁回收。
g0
g0是与m绑定的特殊的goroutine,用于其他的g之间的调度管理。当g0找到要执行的g之后会调用gogo函数将执行权交给当前g,当 g 需要主动让渡或被动调度时会调用m_call函数来把执行权重新交给g0
四种调度类型
-
主动调度:由
用户主动发起,通过runtime.Gosched方法来实现主动让出当前P的执行权。当前g会由_Grunning状态切换成_Grunnable状态,然后被投递到全局队列中。然后执行权回归到g0,g0会继续寻找下一个可执行的g -
被动调度:由于一些客观的因素导致当前的g不得不
陷入阻塞的状态(加锁,channel)。当前g通过gopark方法由_Grunning状态切换成_Gwaiting状态,这个G会由网络轮询器接手,同时将执行权交给g0。goready方法会将当前g从阻塞状态中恢复,重新进入等待执行的状态。然后这个g会被优先加入到唤醒这个g的P的本地队列当中同时优先被调度。 -
正常调度:执行完成当前g之后会将这个g置为_Gdead状态,并发起下一轮的调度。
-
抢占调度:当某一个g发生
系统调用时并超过了一定的时长就会被感知到,然后这个g会和P进行解绑留下g和m绑定(hand off),然后P去寻找其他的空闲m,若没有空闲的就会创建一个新的M。抢占这个动作不再由g0完成,而是有一个全局监控者(monitor g)完成的。因为发起系统调用时需要打破用户态的边界进入内核态,此时 m 也会因系统调用而陷入僵直,无法主动完成抢占调度的行为。
findRunnable函数(寻找可执行的g)
-
首先P如果执行到了第61次,会从全局队列中获取一个g来执行,并将一个全局队列中的g填充到P的本地队列中。如果本地队列已经满了,会将本地的一半g放回到全局队列中缓解本地压力。
-
然后从本地队列中尝试寻找可执行的g,如果有,会尝试加锁获取。由于窃取动作发生的频率不是很高,所以一般都能拿到锁,所以说P的本地队列是接近无锁化的。
-
如果本地队列没有可执行的g,会尝试加锁从全局队列中获取。
-
如果本地和全局都没有,会获取准备就绪的网络协程。
-
最后才会尝试去窃取其他P的一半的g(work-stealing机制),会进行4次尝试,其中某次成功获取到之后就会直接返回