Featured image of post Go语言GMP模型

Go语言GMP模型

Go语言GMP模型个人学习笔记

简介

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

四种调度类型

  1. 主动调度:由 用户 主动发起,通过 runtime.Gosched 方法来实现主动让出当前P的执行权。当前g会由_Grunning状态切换成_Grunnable状态,然后被投递到 全局队列 中。然后执行权回归到g0,g0会继续寻找下一个可执行的g

  2. 被动调度:由于一些客观的因素导致当前的g不得不 陷入阻塞 的状态(加锁,channel)。当前g通过 gopark 方法由_Grunning状态切换成_Gwaiting状态,这个G会由 网络轮询器 接手,同时将执行权交给g0。goready方法会将当前g从阻塞状态中恢复,重新进入等待执行的状态。然后这个g会被优先加入到 唤醒这个g 的P的本地队列当中同时优先被调度。

  3. 正常调度:执行完成当前g之后会将这个g置为_Gdead状态,并发起下一轮的调度。

  4. 抢占调度:当某一个g发生 系统调用 时并超过了一定的时长就会被感知到,然后这个g会和P进行解绑留下g和m绑定(hand off),然后P去寻找其他的空闲m,若没有空闲的就会创建一个新的M。抢占这个动作不再由g0完成,而是有一个全局监控者(monitor g)完成的。因为发起系统调用时需要打破用户态的边界进入内核态,此时 m 也会因系统调用而陷入僵直,无法主动完成抢占调度的行为。

findRunnable函数(寻找可执行的g)

  1. 首先P如果执行到了第61次,会从全局队列中获取一个g来执行,并将一个全局队列中的g填充到P的本地队列中。如果本地队列已经满了,会将本地的一半g放回到全局队列中缓解本地压力。

  2. 然后从本地队列中尝试寻找可执行的g,如果有,会尝试加锁获取。由于窃取动作发生的频率不是很高,所以一般都能拿到锁,所以说P的本地队列是接近无锁化的。

  3. 如果本地队列没有可执行的g,会尝试加锁从全局队列中获取。

  4. 如果本地和全局都没有,会获取准备就绪的网络协程。

  5. 最后才会尝试去窃取其他P的一半的g(work-stealing机制),会进行4次尝试,其中某次成功获取到之后就会直接返回

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计