Goroutine调度机制

一、什么是Goroutine?

Goroutine是go中最为关键的一个设计,在go提供的各种基础包中都能看到它的身影,goroutine实际上就是go语言提供的协程,但是此处要注意,这里的协程并不是传统意义上的协程。传统意义上的协程是指用户态线程,但是go所实现的协程实际上是两级线程模型,关于线程模型可以参考(https://blog.csdn.net/gatieme/article/details/51892437添加链接描述)。

二、GPM模型

在这里插入图片描述
Goroutine的调度可以用GPM模型来解释,如图,以下是模型的解释:

  • G
    Goroutine,每个Goroutine对应一个G结构体,G存储Goroutine的运行堆栈、状态以及任务函数,可重用。
  • P
    Processor,逻辑处理器,G只有绑定到P上才可以被调度,存放Goroutine队列(local runqueue),P的数量决定了系统真正可并行的最大G数量,P的数量由GOMAXPROCS()决定。
  • M
    OS线程,G所依赖的系统级线程,亦即真正执行计算的资源,M里面维护了小对象内存,当前执行的goroutine,以及其他很多信息。
  • Local Runqueue
    由P所维护的G队列,每次从local runqueue中取出可执行的Goroutine
  • Global Runbqueue
    由go调度器所维护的全局G队列,当Local Runqueue中没有G的时候,P就会从Global Runqueue中取出可执行的G

三、Goroutine调度机制

明白以上名词的含义后,接下来就是真正的Goroutine的调度机制了。

首先需要知道G-P-M各自是如何被创建出来的

  1. G 在可执行函数前面加关键字go即可,这样便创建出了一个Goroutine,创建出的Goroutine会进入P所维护的Local Runqueue
  2. P 指定GOMAXPROCS之后,会在程序运行之初创建好对应数目的P
  3. M 当满足以下三个条件以后,M就会被创建:
    队列中G太多
    系统级线程M太少
    有空闲的P

M&P调度

P是在程序运行之初就创建好的,数量由GOMAXPROCS决定(最大256个),从go1.5以后默认即CPU的核数,1.5之前默认是一个。P绑定到M上执行运算,当一个OS线程也就是一个M陷入阻塞的时候,会释放出P,P转而寻找另一个M(M可能是被新创建,也可能来自于线程缓存),继续执行其他G,如果没有其他的idle M,但是P的local runqueue中仍有G需要执行,就会创建一个新的M。

当上述阻塞完成后,G会尝试寻找一个idle的P进入它的Local Runqueue中恢复执行,如果没有找到,G就会进入Global Runqueue,等待其他P从队列中取出。

对M&P对调度有了一个大概的了解以后,我们再继续深入理解上述的阻塞,什么情况下会阻塞呢?如下[1]:

  1. blocking syscall (for example opening a file)
  2. network input
  3. channel operations
  4. primitives in the sync package

这四种场景又可以分为两种类型:
用户态阻塞/唤醒
当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为runnable,尝试加入G2所在P的runnext,然后再是P的Local队列和Global队列。

系统调用阻塞
当G被阻塞在某个系统调用上时,此时G会阻塞在_Gsyscall状态,M也处于 block on syscall 状态,此时的M可被抢占调度:执行该G的M会与P解绑,而P则尝试与其它idle的M绑定,继续执行其它G。如果没有其它idle的M,但P的Local队列中仍然有G需要执行,则创建一个新的M;当系统调用完成后,G会重新尝试获取一个idle的P进入它的Local队列恢复执行,如果没有idle的P,G会被标记为runnable加入到Global队列。

P&G调度

G维护在P所维持的Local Runqueue以及调度器维持的Global Runqueue中。
一、首先需要搞清楚Local Runqueue的入队出队操作。

  • 出队
    首先讲出队,每次执行一个G,P会从Local Runqueue中获取一个G执行,此为出队操作。
  • 入队
    每次用go关键字创建出来的Goroutine会进入P的Local Runqueue中,包括上述用户态阻塞后被唤醒的G的操作为入队操作。
  • 其次,当一个P的Local Runqueue中没有可供执行的G后,该P会随机从其他P的队列中拿一半的G出来,这叫work stealing算法,这又是另一种入队出队操作。

二、然后讲Global Runqueue的入队出队操作

  • 入队
    当M所运行的阻塞状态的G重新唤醒后,如果没有获取到空闲的P,这个G就会进入Global Runqueue中,此为入队。
  • 出队
    其他P没有可执行的G之后描绘直接从Global Runqueue中取,此为出队。

总结:P调度G的时候,首先从P的Local Runqueue中获取G,如果Local Queue中没有的话,就从Global Runqueue中获取,如果Global Runqueue中也没有的话,就随机从其他P的Local Runqueue中偷一半的G出来。

发布了158 篇原创文章 · 获赞 119 · 访问量 81万+

猜你喜欢

转载自blog.csdn.net/u013474436/article/details/104760880