G、P、M 是 Go 调度器的三个核心组件,各司其职。在它们精密地配合下,Go 调度器得以高效运转,这也是 Go 天然支持高并发的内在动力

G

先看 G,取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值,例如 IP 寄存器,以便在轮到本 goroutine 执行时,CPU 知道要从哪一条指令处开始执行。
当 goroutine 被调离 CPU 时,调度器负责把 CPU 寄存器的值保存在 g 对象的成员变量之中。

当 goroutine 被调度起来运行时,调度器又负责把 g 对象的成员变量所保存的寄存器值恢复到 CPU 的寄存器。

本系列使用的代码版本是 1.9.2,来看一下 g 的源码:

  1. type g struct {
  2. // goroutine 使用的栈
  3. stack stack // offset known to runtime/cgo
  4. // 用于栈的扩张和收缩检查,抢占标志
  5. stackguard0 uintptr // offset known to liblink
  6. stackguard1 uintptr // offset known to liblink
  7. _panic *_panic // innermost panic - offset known to liblink
  8. _defer *_defer // innermost defer
  9. // 当前与 g 绑定的 m
  10. m *m // current m; offset known to arm liblink
  11. // goroutine 的运行现场
  12. sched gobuf
  13. syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
  14. syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
  15. stktopsp uintptr // expected sp at top of stack, to check in traceback
  16. // wakeup 时传入的参数
  17. param unsafe.Pointer // passed parameter on wakeup
  18. atomicstatus uint32
  19. stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
  20. goid int64
  21. // g 被阻塞之后的近似时间
  22. waitsince int64 // approx time when the g become blocked
  23. // g 被阻塞的原因
  24. waitreason string // if status==Gwaiting
  25. // 指向全局队列里下一个 g
  26. schedlink guintptr
  27. // 抢占调度标志。这个为 true 时,stackguard0 等于 stackpreempt
  28. preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
  29. paniconfault bool // panic (instead of crash) on unexpected fault address
  30. preemptscan bool // preempted g does scan for gc
  31. gcscandone bool // g has scanned stack; protected by _Gscan bit in status
  32. gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove?
  33. throwsplit bool // must not split stack
  34. raceignore int8 // ignore race detection events
  35. sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
  36. // syscall 返回之后的 cputicks,用来做 tracing
  37. sysexitticks int64 // cputicks when syscall has returned (for tracing)
  38. traceseq uint64 // trace event sequencer
  39. tracelastp puintptr // last P emitted an event for this goroutine
  40. // 如果调用了 LockOsThread,那么这个 g 会绑定到某个 m 上
  41. lockedm *m
  42. sig uint32
  43. writebuf []byte
  44. sigcode0 uintptr
  45. sigcode1 uintptr
  46. sigpc uintptr
  47. // 创建该 goroutine 的语句的指令地址
  48. gopc uintptr // pc of go statement that created this goroutine
  49. // goroutine 函数的指令地址
  50. startpc uintptr // pc of goroutine function
  51. racectx uintptr
  52. waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
  53. cgoCtxt []uintptr // cgo traceback context
  54. labels unsafe.Pointer // profiler labels
  55. // time.Sleep 缓存的定时器
  56. timer *timer // cached timer for time.Sleep
  57. gcAssistBytes int64
  58. }

g 结构体关联了两个比较简单的结构体,stack 表示 goroutine 运行时的栈:

  1. // 描述栈的数据结构,栈的范围:[lo, hi)
  2. type stack struct {
  3. // 栈顶,低地址
  4. lo uintptr
  5. // 栈低,高地址
  6. hi uintptr
  7. }

Goroutine 运行时,光有栈还不行,至少还得包括 PC,SP 等寄存器,gobuf 就保存了这些值:

  1. type gobuf struct {
  2. // 存储 rsp 寄存器的值
  3. sp uintptr
  4. // 存储 rip 寄存器的值
  5. pc uintptr
  6. // 指向 goroutine
  7. g guintptr
  8. ctxt unsafe.Pointer // this has to be a pointer so that gc scans it
  9. // 保存系统调用的返回值
  10. ret sys.Uintreg
  11. lr uintptr
  12. bp uintptr // for GOEXPERIMENT=framepointer
  13. }

sudog

当 g 遇到阻塞,或需要等待的场景时,会被打包成 sudog 这样一个结构。一个 g 可能被打包为多个 sudog 分别挂在不同的等待队列上:

  1. // sudog 代表在等待列表里的 g,比如向 channel 发送/接收内容时
  2. // 之所以需要 sudog 是因为 g 和同步对象之间的关系是多对多的
  3. // 一个 g 可能会在多个等待队列中,所以一个 g 可能被打包为多个 sudog
  4. // 多个 g 也可以等待在同一个同步对象上
  5. // 因此对于一个同步对象就会有很多 sudog 了
  6. // sudog 是从一个特殊的池中进行分配的。用 acquireSudog 和 releaseSudog 来分配和释放 sudog
  7. type sudog struct {
  8. // 之后的这些字段都是被该 g 所挂在的 channel 中的 hchan.lock 来保护的
  9. // shrinkstack depends on
  10. // this for sudogs involved in channel ops.
  11. g *g
  12. // isSelect 表示一个 g 是否正在参与 select 操作
  13. // 所以 g.selectDone 必须用 CAS 来操作,以胜出唤醒的竞争
  14. isSelect bool
  15. next *sudog
  16. prev *sudog
  17. elem unsafe.Pointer
  18. // 下面这些字段则永远都不会被并发访问
  19. // 对于 channel 来说,waitlink 只会被 g 访问
  20. // 对于信号量来说,所有的字段,包括上面的那些字段都只在持有 semaRoot 锁时才可以访问
  21. acquiretime int64
  22. releasetime int64
  23. ticket uint32
  24. parent *sudog // semaRoot binary tree
  25. waitlink *sudog // g.waiting list or semaRoot
  26. waittail *sudog // semaRoot
  27. c *hchan // channel
  28. }

M

再来看 M,取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人。结构体 m 就是我们常说的 M,它保存了 M 自身使用的栈信息、当前正在 M 上执行的 G 信息、与之绑定的 P 信息……
当 M 没有工作可做的时候,在它休眠前,会“自旋”地来找工作:检查全局队列,查看 network poller,试图执行 gc 任务,或者“偷”工作。

有个比较常见的属于:TLS(thread local storage) 线程本地存储
结构体 m 的源码如下:

  1. // m 代表工作线程,保存了自身使用的栈信息
  2. type m struct {
  3. // 记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用
  4. // 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换
  5. g0 *g // goroutine with scheduling stack/
  6. morebuf gobuf // gobuf arg to morestack
  7. divmod uint32 // div/mod denominator for arm - known to liblink
  8. // Fields not known to debuggers.
  9. procid uint64 // for debuggers, but offset not hard-coded
  10. gsignal *g // signal-handling g
  11. sigmask sigset // storage for saved signal mask
  12. // 通过 tls 结构体实现 m 与工作线程的绑定
  13. // 这里是线程本地存储
  14. tls [6]uintptr // thread-local storage (for x86 extern register)
  15. mstartfn func()
  16. // 指向正在运行的 gorutine 对象
  17. curg *g // current running goroutine
  18. caughtsig guintptr // goroutine running during fatal signal
  19. // 当前工作线程绑定的 p
  20. p puintptr // attached p for executing go code (nil if not executing go code)
  21. nextp puintptr
  22. id int32
  23. mallocing int32
  24. throwing int32
  25. // 该字段不等于空字符串的话,要保持 curg 始终在这个 m 上运行
  26. preemptoff string // if != "", keep curg running on this m
  27. locks int32
  28. softfloat int32
  29. dying int32
  30. profilehz int32
  31. helpgc int32
  32. // 为 true 时表示当前 m 处于自旋状态,正在从其他线程偷工作
  33. spinning bool // m is out of work and is actively looking for work
  34. // m 正阻塞在 note 上
  35. blocked bool // m is blocked on a note
  36. // m 正在执行 write barrier
  37. inwb bool // m is executing a write barrier
  38. newSigstack bool // minit on C thread called sigaltstack
  39. printlock int8
  40. // 正在执行 cgo 调用
  41. incgo bool // m is executing a cgo call
  42. fastrand uint32
  43. // cgo 调用总计数
  44. ncgocall uint64 // number of cgo calls in total
  45. ncgo int32 // number of cgo calls currently in progress
  46. cgoCallersUse uint32 // if non-zero, cgoCallers in use temporarily
  47. cgoCallers *cgoCallers // cgo traceback if crashing in cgo call
  48. // 没有 goroutine 需要运行时,工作线程睡眠在这个 park 成员上,
  49. // 其它线程通过这个 park 唤醒该工作线程
  50. park note
  51. // 记录所有工作线程的链表
  52. alllink *m // on allm
  53. schedlink muintptr
  54. mcache *mcache
  55. lockedg *g
  56. createstack [32]uintptr // stack that created this thread.
  57. freglo [16]uint32 // d[i] lsb and f[i]
  58. freghi [16]uint32 // d[i] msb and f[i+16]
  59. fflag uint32 // floating point compare flags
  60. locked uint32 // tracking for lockosthread
  61. // 正在等待锁的下一个 m
  62. nextwaitm uintptr // next m waiting for lock
  63. needextram bool
  64. traceback uint8
  65. waitunlockf unsafe.Pointer // todo go func(*g, unsafe.pointer) bool
  66. waitlock unsafe.Pointer
  67. waittraceev byte
  68. waittraceskip int
  69. startingtrace bool
  70. syscalltick uint32
  71. // 工作线程 id
  72. thread uintptr // thread handle
  73. // these are here because they are too large to be on the stack
  74. // of low-level NOSPLIT functions.
  75. libcall libcall
  76. libcallpc uintptr // for cpu profiler
  77. libcallsp uintptr
  78. libcallg guintptr
  79. syscall libcall // stores syscall parameters on windows
  80. mOS
  81. }

P

再来看 P,取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。
一个 M 只有绑定 P 才能执行 goroutine,当 M 被阻塞时,整个 P 会被传递给其他 M ,或者说整个 P 被接管。

  1. // p 保存 go 运行时所必须的资源
  2. type p struct {
  3. lock mutex
  4. // 在 allp 中的索引
  5. id int32
  6. status uint32 // one of pidle/prunning/...
  7. link puintptr
  8. // 每次调用 schedule 时会加一
  9. schedtick uint32
  10. // 每次系统调用时加一
  11. syscalltick uint32
  12. // 用于 sysmon 线程记录被监控 p 的系统调用时间和运行时间
  13. sysmontick sysmontick // last tick observed by sysmon
  14. // 指向绑定的 m,如果 p 是 idle 的话,那这个指针是 nil
  15. m muintptr // back-link to associated m (nil if idle)
  16. mcache *mcache
  17. racectx uintptr
  18. deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go)
  19. deferpoolbuf [5][32]*_defer
  20. // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
  21. goidcache uint64
  22. goidcacheend uint64
  23. // Queue of runnable goroutines. Accessed without lock.
  24. // 本地可运行的队列,不用通过锁即可访问
  25. runqhead uint32 // 队列头
  26. runqtail uint32 // 队列尾
  27. // 使用数组实现的循环队列
  28. runq [256]guintptr
  29. // runnext 非空时,代表的是一个 runnable 状态的 G,
  30. // 这个 G 被 当前 G 修改为 ready 状态,相比 runq 中的 G 有更高的优先级。
  31. // 如果当前 G 还有剩余的可用时间,那么就应该运行这个 G
  32. // 运行之后,该 G 会继承当前 G 的剩余时间
  33. runnext guintptr
  34. // Available G's (status == Gdead)
  35. // 空闲的 g
  36. gfree *g
  37. gfreecnt int32
  38. sudogcache []*sudog
  39. sudogbuf [128]*sudog
  40. tracebuf traceBufPtr
  41. traceSwept, traceReclaimed uintptr
  42. palloc persistentAlloc // per-P to avoid mutex
  43. // Per-P GC state
  44. gcAssistTime int64 // Nanoseconds in assistAlloc
  45. gcBgMarkWorker guintptr
  46. gcMarkWorkerMode gcMarkWorkerMode
  47. runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point
  48. pad [sys.CacheLineSize]byte
  49. }

通常情况下(在程序运行时不调整 P 的个数),P 只会在上图中的四种状态下进行切换。 当程序刚开始运行进行初始化时,所有的 P 都处于 _Pgcstop 状态, 随着 P 的初始化(runtime.procresize),会被置于 _Pidle

当 M 需要运行时,会 runtime.acquirep 来使 P 变成 Prunning 状态,并通过 runtime.releasep 来释放。 当 G 执行时需要进入系统调用,P 会被设置为 _Psyscall, 如果这个时候被系统监控抢夺(runtime.retake),则 P 会被重新修改为 _Pidle。 如果在程序运行中发生 GC,则 P 会被设置为 _Pgcstop, 并在 runtime.startTheWorld 时重新调整为 _Prunning。

schedt

全局调度器,全局只有一个 schedt 类型的实例:

  1. type schedt struct {
  2. // accessed atomically. keep at top to ensure alignment on 32-bit systems.
  3. goidgen uint64
  4. lastpoll uint64
  5. lock mutex
  6. // When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
  7. // sure to call checkdead().
  8. // idle状态的m
  9. midle muintptr // idle m's waiting for work
  10. // idle状态的m个数
  11. nmidle int32 // number of idle m's waiting for work
  12. // lockde状态的m个数
  13. nmidlelocked int32 // number of locked m's waiting for work
  14. mnext int64 // number of m's that have been created and next M ID
  15. // m允许的最大个数
  16. maxmcount int32 // maximum number of m's allowed (or die)
  17. nmsys int32 // number of system m's not counted for deadlock
  18. nmfreed int64 // cumulative number of freed m's
  19. // 系统中goroutine的数目,会自动更新
  20. ngsys uint32 // number of system goroutines; updated atomically
  21. // idle的p列表
  22. pidle puintptr // idle p's
  23. // 有多少个状态为idle的p
  24. npidle uint32
  25. // 有多少个m自旋
  26. nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
  27. // Global runnable queue.
  28. // 全局的可运行的g队列
  29. runqhead guintptr
  30. runqtail guintptr
  31. // 全局队列的大小
  32. runqsize int32
  33. // Global cache of dead G's.
  34. // dead的G的全局缓存
  35. gflock mutex
  36. gfreeStack *g
  37. gfreeNoStack *g
  38. ngfree int32
  39. // Central cache of sudog structs.
  40. // sudog的缓存中心
  41. sudoglock mutex
  42. sudogcache *sudog
  43. // Central pool of available defer structs of different sizes.
  44. deferlock mutex
  45. deferpool [5]*_defer
  46. // freem is the list of m's waiting to be freed when their
  47. // m.exited is set. Linked through m.freelink.
  48. freem *m
  49. gcwaiting uint32 // gc is waiting to run
  50. stopwait int32
  51. stopnote note
  52. sysmonwait uint32
  53. sysmonnote note
  54. // safepointFn should be called on each P at the next GC
  55. // safepoint if p.runSafePointFn is set.
  56. safePointFn func(*p)
  57. safePointWait int32
  58. safePointNote note
  59. profilehz int32 // cpu profiling rate
  60. procresizetime int64 // nanotime() of last change to gomaxprocs
  61. totaltime int64 // ∫gomaxprocs dt up to procresizetime
  62. }

G状态

参考:https://studygolang.com/articles/11861
image.png

Grunnable

Golang中,一个协程在以下几种情况下会被设置为 Grunnable状态:

  • 创建:Go 语言中,包括用户入口函数main·main的执行goroutine在内的所有任务,都是通过runtime·newproc -> runtime·newproc1 这两个函数创建的
  • 阻塞任务唤醒:当某个阻塞任务(状态为Gwaiting)的等待条件满足而被唤醒时,会调用runtime·ready,将状态变为Runnable并放入执行队列
  • 其他:另外的路径是从Grunning和Gsyscall状态变换到Grunnable

Grunning

所有状态为Grunnable的任务都可能通过findrunnable函数被调度器(P&M)获取,进而通过execute函数将其状态切换到Grunning, 最后调用runtime·gogo加载其上下文并执行。

Gsyscall

Go运行时为了保证高的并发性能,当会在任务执行OS系统调用前,先调用runtime·entersyscall函数将自己的状态置为Gsyscall——如果系统调用是阻塞式的或者执行过久,则将当前M与P分离——当系统调用返回后,执行线程调用runtime·exitsyscall尝试重新获取P,如果成功且当前任务没有被抢占,则将状态切回Grunning并继续执行;否则将状态置为Grunnable,等待再次被调度执行。

Gwaiting

当一个任务需要的资源或运行条件不能被满足时,需要调用runtime·gopark函数进入该状态,之后除非等待条件满足,否则任务将一直处于等待状态不能执行。除了之前举过的channel的例子外,Go语言的定时器、网络IO操作都可能引起任务的阻塞。

Gdead

最后,当一个任务执行结束后,会调用runtime·goexit结束自己的生命——将状态置为Gdead,并将结构体链到一个属于当前P的空闲G链表中,以备后续使用。

M状态

image.png

P状态

image.png