抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

GMP模型

  • G:即goroutine协程,每个Go关键字都会创建一个协程,协程在当创建的时候大约只占2MB,在不断的运行中可以按需增大(最大1GB)和缩小。
  • M:即操作系统线程,Go中默认最多创建10000线程,同时每个Go协程都需要M上才能运行。
  • P:即调度器,负责将协程调度到线程上,默认数量为CPU核心数,每一个M都与一个P对应。

本地队列和全局队列

在P调度器中具有两个队列,分别是Local queue本地队列和Global queue全局队列。

每个P调度器都会有一个本地队列,而全局队列则是全局唯一。

当我们使用go关键字创建一个协程时,会将新建的协程优先放置对应P的本地队列中,本地队列是一个基于数组实现的循环链表,最多可以存储256个协程。因此当本地队列存储的协程超过256个时就会将新建的协程放置全局队列中。

work stealing和hand off机制

  • work stealing:当M线程所绑定的P调度器的本地队列没有可以运行的G协程认为时,会从其他的P调度器偷取G协程任务,放置进自己的本地队列中
  • hand off:当在M线程执行G协程进行系统调用等堵塞时,M线程会释放所绑定的P处理器,将P交给其他空闲的M线程来执行,以便更好的利用CPU的资源。当堵塞的协程恢复后会尝试获得原来所绑定的P处理器,如果获得不到则尝试获得其他空闲的P处理器,如果还获得不到空闲处理器则将当前M线程休眠,随后将G放置进全局队列中。

M0和G0

  • M0:它是应用程序启动后编号为0的线程,它负责执行初始化操作和启动一个人G0。
  • G0:它是每一个M线程都会创建的第一个G协程,负责调度其他的协程。

Go调度大致流程

  1. 创建第一个M线程 M0
  2. 创建第一个G协程 G0
  3. 进行初始化操作,例如设置M线程的最大个数、初始化栈和内存、设置GOMAXPROCS参数
  4. 创建main函数的G协程
  5. 启动M0
  6. M不断获得P中的G来执行相关任务

M会优先会所绑定的P中的本地队列获取G,当本地队列为空时,就会尝试从全局队列里获得一批协程,如果还为空最会使用上面所讲的work stealing策略,如果还为空则会自旋或者进入休眠状态。

同时一个协程在运行中最多占用CPU 10ms的时间,以让其他的协程也有运行的机会。

一道GMP面试题

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
func TestGMP() {
runtime.GOMAXPROCS(1)
num := 10
waitGroup := sync.WaitGroup{}
waitGroup.Add(num)

for i := 0; i < num; i++ {
j := i
go func() {
defer waitGroup.Done()
fmt.Println(j)
}()
}
waitGroup.Wait()
}

输出值:
9
0
1
2
3
4
5
6
7
8

上面的代码的输出顺序一直是固定的,这是因为我们使用runtime.GOMAXPROCS(1)将P调度器的数量设为1,这样所有G协程都会进入该P调度器。

当一个协程进入P处理器时,都会先将该处理器放置runnext(即下一个运行的协程),如果runnext已有协程的话,就会将之前的协程放置进本地队列中,然后再将新的协程放进runnext中。

因此j为9的协程总是在runnext的位置,即第一个运行的协程,随后其他的协程根据队列FIFO的顺序,陆续被执行。

学习资料