Go调度系列--GMP是什么?(一)

2023-03-14 12:00:30 浏览数 (2)

前言

做为Go开发者基本上对GMP已经很熟悉,这是Go的核心内容,三个核心部分共同配合下让Go 调度器得以高效运转。结合之前我们对编译和启动流程的总结,现在就更容易从结构和汇编调用的实际函数来进行结合理解,我们先来看Go调度器的组成部分GMP各部分的结构和用处。

注:文中GMP的底层数据结构都在src/runtime/runtime2.go中,每个结构体的字段数比较多,只截取了一部分进行了说明。

G(Goroutine)

G 取自Goroutine的首字母,一个G代表一个Goroutine,Goroutine是一个与其他Goroutines并行运行在同一地址空间的go函数或方法。

它是一个数据结构,这个结构体里面有栈信息,上下文等。它占用了更小的内存空间(2KB内存),同时也降低了上下文切换的开销,Goroutine数量理论上只受内存大小限制。

在使用go 关键字创建一个goroutine时候,会调用runtime.newproc来创建一个Goroutine,在我们将Go程序的启动流程的时候就清楚,它的汇编代码如下,调用的是src/runtime/proc.go的newproc()函数,newproc 方法会切换到调用 newproc1 函数进行 G 的创建。newproc1 方法很长,里面主要是获取 G ,然后对获取到的 G 做一些初始化的工作。

代码语言:javascript复制
// asm_amd64.s
CALL	runtime·newproc(SB)

//proc.go
func newproc(siz int32, fn *funcval) {
 argp := add(unsafe.Pointer(&fn), sys.PtrSize)
 gp := getg()  //返回g实际的结构体指针
 pc := getcallerpc()
 systemstack(func() {
  newg := newproc1(fn, argp, siz, gp, pc)

  _p_ := getg().m.p.ptr()
  runqput(_p_, newg, true)

  if mainStarted {
   wakep()
  }
 })
}

我们看g结构体的源码部分,结构体包含了g使用的栈,可能的panic和defer链、以及运行现场等信息。

代码语言:javascript复制
type g struct {
   stack       stack   // g 使用的栈
   stackguard0 uintptr // offset known to liblink
   stackguard1 uintptr // offset known to liblink

   _panic       *_panic // panic链表
   _defer       *_defer // defer延迟函数链表
   m            *m      // 当前绑定的m
   sched        gobuf
   // 其他运行现场信息
   ...
}

M(Machine)

M是一个线程或称为Machine(取首字母),G 需要调度到 M 上才能运行,M 是真正的执行者,调度器最多可以创建 10000 个线程。

最多只会有 GOMAXPROCS 个活跃线程能够正常运行,因为M需要和P绑定才能运行G,而P的个数取决于设置的GOMAXPROCS,一个M阻塞了就会创建新的M。

M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。

m结构体部分源码如下:

代码语言:javascript复制
// 底层数据结构
type m struct {
 g0      *g     // goroutine with scheduling stack
 divmod  uint32 
 ...
    curg     *g         // M当前绑定的结构体G
 ...
}

这里的g0和curg是两个不同的goroutine。g0 是一个运行时中比较特殊的 Goroutine,它会深度参与运行时的调度过程,包括 Goroutine 的创建、大内存分配和 CGO 函数的执行,curg 是在当前线程上运行的用户 Goroutine。

关于g0的特殊性总结如下:

  1. g0 所有调用栈的goroutine,这是一个比较特殊的goroutine。
  2. 普通的goroutine栈是在Heap分配的可增长的stack,而g0的stack是M对应的线程栈。
  3. 所有调度相关代码,会先切换到该g0 goroutine的栈再执行。

P(Processor)

P理解为处理器,它是M和G的中间层,负责调度M上的等待队列,通过处理器 P 的调度,每一个M都能够执行多个 Goroutine。

P的个数由runtime.GOMAXPROCS进行指定,默认是被设置为可用的CPU核数,比如你有8核处理器,那么P的数量就是默认8个。

P需要和M进行绑定才能执行G,当 M 被阻塞时,整个 P 会被传递给其他 M ,或者说整个 P 被接管。

代码语言:javascript复制
type p struct {
 id          int32
 status      uint32 // one of pidle/prunning/...
 link        puintptr
 schedtick   uint32     // incremented on every scheduler call
 syscalltick uint32     // incremented on every system call
 sysmontick  sysmontick // last tick observed by sysmon
 m           muintptr   // back-link to associated m (nil if idle)
 ...
}

总结

简单介绍了 Go 语言调度器中GMP的底层数据结构,包括线程 M、处理器 P 和 Goroutine G。三者关系:G需要绑定在M上才能运行,M需要绑定P才能运行,M 会从与它绑定的 P 的本地队列获取可运行的 G,还会从其他 P 偷 G。

0 人点赞