首页 > 文章列表 > Go语言中为什么要有GMP调度模型

Go语言中为什么要有GMP调度模型

golang
466 2023-05-10

Go语言中为什么要有GMP调度模型

GMP调度模型是Go的精髓所在,它合理地解决了多线程并发调度协程的效率问题。

GMP是什么

首先得清楚,GMP各代指什么东西。

  • G: Goroutine的缩写,指协程,运行在线程上。

  • M: Machine的缩写,即thead,线程,循环的调度协程并执行。

  • P: Processor的缩写,指处理器,将协程储存到本地队列,并为线程提供未休眠的可用的协程,

线程M各自持有一个处理器P,当要获取协程时优先从P中获取,于是GMP模型图解如下:

大致流程是,线程M从P的队列中获取协程,如果获取不到,就会从全局队列中去竞争锁来获取。

处理器P

协程G和线程M结构在前几篇已经讲解了,这里解析一下处理器P。

作用

处理器P储存着一批协程,使得线程M可以无锁的从中获取协程,而无需与其他线程去竞争全局队列中的协程,从而提高调度协程效率。

源码解析

p结构体源码在src\runtime\runtime2.go中,这里展示部分重要字段。

type p struct {

   ...

   m           muintptr   // back-link to associated m (nil if idle)

   // Queue of runnable goroutines. Accessed without lock.

   runqhead uint32

   runqtail uint32

   runq     [256]guintptr

   runnext guintptr

   ...

}
  • m为处理器p所属的线程

  • runq是一个储存协程的队列

  • runqheadrunqtail表示队列的头尾指针

  • runnext指向下一个可运行的协程

线程M与处理器P是如何协作的?

src\runtime\proc.go中,有一个schedule方法,这是线程运行的第一个函数。这函数中,线程需要获取到可运行的协程,代码如下:

func schedule() {    

    ...

    // 寻找一个可运行的协程

    gp, inheritTime, tryWakeP := findRunnable() 

    ...

}
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {

    // 从本地队列中获取协程

    if gp, inheritTime := runqget(pp); gp != nil {

       return gp, inheritTime, false

    }



    // 本地队列拿不到则从全局队列中获取协程

    if sched.runqsize != 0 {

       lock(&sched.lock)

       gp := globrunqget(pp, 0)

       unlock(&sched.lock)

       if gp != nil {

          return gp, false, false

       }

    }

}

从本地队列中获取协程

func runqget(pp *p) (gp *g, inheritTime bool) {

   next := pp.runnext // 队列中下一个可运行的协程

   if next != 0 && pp.runnext.cas(next, 0) {

      return next.ptr(), true

   }

   ...

}

那如果本地队列和全局队列中都没有协程了怎么办呢,难道就让线程这么闲着?

这时候处理器P就会任务窃取,从其他线程的本地队列中窃取一些任务,美其名曰分担其他线程的压力,还提高了自己线程的利用率。

源码在src\runtime\proc.go\stealWork中,感兴趣可以看看。

新建的协程该分配到哪?

新建的协程该分配到本地还是全局队列呢,得分情况:

  • Go认为新协程的优先级高,于是先寻找本地队列放入,而且还插队。

  • 本队队列满了才放入全局队列。

实际流程为:

  1. 随机寻找P

  2. 将新协程放入P的runnext中,意味着下一个就运行该协程,插队了

  3. 若P的协程满了,则放入全局队列

源码在src\runtime\proc.go\newproc函数中。

// Create a new g running fn.

// Put it on the queue of g's waiting to run.

// The compiler turns a go statement into a call to this.

func newproc(fn *funcval) {

   gp := getg()

   pc := getcallerpc()

   systemstack(func() {

      newg := newproc1(fn, gp, pc) // 创建新协程



      pp := getg().m.p.ptr()

      runqput(pp, newg, true) // 寻找本地队列放入



      if mainStarted {

         wakep()

      }

   })

}