GMP调取器模型设计
WsWHL Lv3

GPM代表了三个角色,分别是Goroutine、Processor、Machine。

  • G — 就是咱们常用的用go关键字创建的执行体,它对应一个结构体g,结构体里保存了goroutine的堆栈信息
  • M — 表示操作系统的线程,它由操作系统的调度器调度和管理
  • P — 表示处理器,它可以被看做运行在线程上的本地调度器

G

Goroutine 是 Go 语言调度器中待执行的任务,它在运行时调度器中的地位与线程在操作系统中差不多,但是它占用了更小的内存空间,也降低了上下文切换的开销。

Goroutine 只存在于 Go 语言的运行时,它是 Go 语言在用户态提供的线程,作为一种粒度更细的资源调度单元,如果使用得当能够在高并发的场景下更高效地利用机器的 CPU。

M

Go 语言并发模型中的 M 是操作系统线程。调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用),最多只会有 GOMAXPROCS 个活跃线程能够正常运行。

默认情况下GOMAXPROCS被设置为内核数,假如有四个内核,那么默认就创建四个线程,每一个线程对应一个runtime.m结构体。线程数等于CPU个数的原因是,每个线程分配到一个CPU上就不至于出现线程的上下文切换,可以保证系统开销降到最低。

P

调度器中的处理器 P 是线程和 Goroutine 的中间层,它能提供线程需要的上下文环境,也会负责调度线程上的等待队列,通过处理器 P 的调度,每一个内核线程都能够执行多个 Goroutine,它能在 Goroutine 进行一些 I/O 操作时及时让出计算资源,提高线程的利用率。

因为调度器在启动时就会创建 GOMAXPROCS 个处理器,所以 Go 语言程序的处理器数量一定会等于 GOMAXPROCS,这些处理器会绑定到不同的内核线程上。

调度流程

  1. P和M一对一关系,每个M关联一个P,M和P的数量与GOMAXPROCS的值一致。
  2. 创建G时会优先交给P,每个P有个本地队列,最多可容纳256个G,当P本地队列塞满了就会将G放到全局对列中。
  3. 线程M会从P本地队列获取G执行调度,每调度61次就会去全局队列获取G调度,防止全局队列中的G迟迟无法调度问题。
  4. 处理器P本地队列的任务G调度完了以后,就会去全局队列获取待执行的任务G,如果全局队列空了,就会去其他处理器P中偷待执行的任务G。
  5. 如果某个G处于阻塞状态,线程M就会去获取其他任务G执行调度。
  6. 如果G进行了系统调用syscall,M也会跟着进入系统调用状态,处理器P不会处于等待状态,而是会去找其他比较闲的M执行其他任务G。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"runtime"
"sync"
)

func main() {
runtime.GOMAXPROCS(1)

wg := &sync.WaitGroup{}
wg.Add(3)

go func(i int) {
fmt.Println(i)
wg.Done()
}(1)

go func(i int) {
fmt.Println(i)
wg.Done()
}(2)

go func(i int) {
fmt.Println(i)
wg.Done()
}(3)

wg.Wait()
}

打印输出如下:

3
1
2

原理:

此处只有一个线程M和对应一个处理器P,每个处理器P有个待执行的runnext,表示接下来调度的G,创建G时会优先给到runnext,如果继续添加G就会将原来的G挤走到P的本地队列runq(最多256个)中,依此类推2把1挤走,3把2挤走,最后runnext为3,3调度完了从本地队列获取1执行,1执行完了获取2执行,所以得到如上输出结果。.

 评论