GMP调度

154 阅读4分钟

之前春招的时候写过一篇GMP文章,现在过了一年了重新梳理一下GMP过程。温故而知新,这次复习还是有新的收货的。

线程池

其他语言(java)没有协程的概念,但是也需要实现高并发的业务,一般会采用线程池的概念。就是会在线程池中维护一定数量的线程,新任务将不会创建新的线程,而是放进任务队列中。线程池中线程将不断从任务队列中取任务并执行

但是如果频繁的发生系统调用,线程就会被阻塞,线程池中的可以用线程就会越来越少,运行效率就会变低

golang goroutine调度器

线程数过多,协程上下文切换就会消耗很多的cpu资源。golang为了解决这种问题,设计了gorutine机制,goroutine可以在线程中进行调度,而且上下文切换开销很小

goroutine

goroutine占用的资源很小,每个goroutine栈空间大小默认是2kb,后续会根据需求动态扩容

因此一个go程序中可以创建成千上万个goroutine,而将这些goroutine按照一定算法放到'cpu'上执行,就被称为goroutine调度器

不过对于操作系统来说,cpu对协程是无感知的,只对线程负责,甚至可以说它都不知道有goroutine存在。那么,goroutine的调度就要依靠自己来完成。在操作系统层面,线程要竞争的资源是物理cpu。goroutine竞争的cpu资源是线程。

就是多个goroutine竞争多个线程,这就是GMP的M:N模型。goroutine挂靠在M上执行,而且换协程只需要切换G就可以了,这样切换的话就不会消耗大量的cpu资源,而仅仅只需要goroutine自己调度即可。

G-M 模型

G:goroutine抽象成结构

M:实体线程抽象成的结构

全局队列:所有的G都在这个全局队列上,go调度器会将G调度到M上执行

缺点:这个全局队列是加锁的,G的创建、调度也都需要上锁,频繁的锁操作就会导致调度延迟和性能损耗

GMP模型

后来设计师就改进了这个模型,增加了一个中间层,就是现在大家熟知的GMP

G:就是goroutine,储存了goroutine的堆栈信息和任务函数

M:就是实体线程抽象成的结构,G只有挂靠在M上才能真正的执行

P:就是process逻辑处理器,P维护了一个本地队列,里面存放了G,p的数量就决定了最大并行的数量。P同时也控制了G的调度

调度策略

  • 队列轮转机制

    • p会周期性的调度本地队列中的G到M中执行,执行完毕后会把G放到队尾,然后调本地队列中下一个G执行。
    • zP除了维护本地的G队列之外,还会周期性的检查全局队列是否有待运行的G执行。这些G的来源主要是从系统调用中恢复的G,之所以会周期性的检查,就是为了防止这个G饿死
  • 工作量窃取机制

    • 当P的本地队列中的G都执行完毕之后,会先去查询全局队列,如果全局队列中也没有待运行的G,就会从别的P偷取一部分G过来,一次偷一半
  • 系统调用

    • P的个数默认约等于CPU核数,可以通过setmaxprocess设置。当发生系统调用的时候,M就会释放P。某个空闲的M1会获取P,进而继续执行剩下P本地队列中剩下的G,这样能保证充分利用CPU核数
    • 当系统调用结束之后,原本的M会和G解绑,此时如果有空闲的P,M就会获取一个P,然后继续执行G。如果没有空闲的P,这个G就会被放入全局队列中,等待被P捕获

调度机制

  • 主动调度:业务程序主动调用 runtime.Gosched() 函数,会主动让出处理器资源,从g切换到g0

  • 被动调度:

    • 因为非系统调用而发生的阻塞,比如读写channel时发生阻塞、网络IO操作时。G会被放入某个等待队列上,这时候M会尝试运行P的下一个可运行的G。如果没有,M就会解绑P,然后进入挂起状态。当Io或者channel操作完成字后,G会从等待队列中被唤醒,并放入某个P的本地队列中,绑定一个M后继续执行
    • 系统调用发生阻塞,hand of机制
  • 抢占调度:main函数会启动一个sysmon监控线程,如果一个 G 任务运行 10ms,sysmon 就会认为它的运行时间太久而发出抢占式调度的请求,一旦 G 的抢占标志位被设为 true,就会通过retake 函数对goroutine发起抢占。

参考:Go 协程(goroutine)调度原理

# 16. Go调度器系列解读(三):GMP 模型调度时机