GMP模型

什么是GMP模型?

  • G:gorotine(协程)
  • M:machine(内核线程)
  • P:processor(调度器)

Go语言运行时,通过核心元素G,M,P 和 自己的调度器,实现了自己的并发线程模型。调度器通过对G,M,P的调度实现了两级线程模型中操作系统内核之外的调度任务。

Golang调度器由来

Goroutine调度器器的GMP模型的设计思想

GMP模型简介

  • G:goroutine(协程)
  • M:machine(内核线程)
  • P:processor(调度器,负责G和M之间的调度,不是GMP模型的调度)

全局队列:存放等待运行的G

P的本地队列:

  • 存放等待运行的G,
  • 有数量限制(不超过256个),
  • 新创建的G优先放置在本地队列,存满了会放在全局队列

P列表:程序启动时创建,默认数量为cpu线程数

M列表:当前操作系统分配到当前Go程序的内核线程数

P和M的数量:

  • P的数量可通过环境变量($GOMAXPROCS)或程序中代码设置runtime.GOMAXPROCS() .
  • M的数量Go语言本身限制为10000,可通过runtime/debug包中的SetMaxThreads函数来设置,
  • 有一个M阻塞,就会创建一个新的M,有M空闲就会回收或者睡眠。

调度器的设计策略

1.复用线程

避免频繁的创建销毁线程,有两种机制 work stealing机制hand off机制

work stealing机制 : 当前P的本地队列和全局队列均无可运行的G时会尝试从其他P的队列中偷取G
hand off机制 : 当本线程正在运行的G发生阻塞时,会将G和M进行绑定,并把P转移给其他的空闲M

2.利用并行

GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个cpu上同时运行

3.抢占

在coroutine中要等待一个协程主动让出CPU才执行下一个协程.

在Go中,一个goroutine最多占⽤用CPU 10ms,防⽌止其他goroutine被饿死.

4.全局G队列列

用于存放G,有锁保护,全局队列中的G是所有P共享的,当P的本地队列为空时,会从全局队列中获取G

调度规则

如果处理器没有任务可处理,它会按以下规则来执行,直到满足某一条规则:
从本地队列获取任务 -> 从其它的处理器的本地队列窃取任务 -> 从全局队列获取任务

  • 为什么优先从其他 P 偷取任务?

减少全局锁竞争: 全局队列是一个全局共享资源,访问它时需要加锁,可能会引发竞争。而本地队列之间没有共享,因此偷取任务时不会引发全局锁的竞争,能够提高性能。
负载均衡: 偷取其他 P 的任务有助于在多个处理器之间更好地均衡任务负载,减少单个处理器繁忙而其他处理器空闲的情况。

go func() 经历了什么

  1. 先创建一个goroutine 也就是G
  2. G会优先 存储在创建G的P的本地本地队列,若队列已满就会存在全局队列
  3. G只能P的调度下运行在M上,M和P一一绑定,当G执行关闭后会在本地队列中取一个G运行,若队列为空,就会从全局队列取一个G,全局队列也为空的话就会从其他绑定的M和P的队列中偷取
  4. M调度G的执行过程是一个循环机制
  5. 当M执行某个G的时候,如果发生了syscall或其他阻塞操作,M会阻塞,如果当前有一些G在运行,runtime就会把这个M与P进行绑定,然后去复用空间的线程或创建一个线程来服务这个P
  6. 当阻塞的M不再阻塞时,G会尝试获取一个空闲的P执行并放入其本地队列,获取不到P的话,这个M就会变成休眠状态,加入到空闲线程中,然后G会放入全局队列中。

M0 和 G0

M0是启动程序后的编号为0的主线 程,这个M对应的实例例会在全局变量量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M⼀样了。

G0是每次启动一个M都会第一个创建的gourtine,G0是仅用于负责调度的G,G0不指向任何可执⾏的函数, 每个M都会有一个⾃⼰的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。

trace 编程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
	f, err := os.Create("trace.out")
	if err != nil {
		panic(err)
	}
	defer f.Close()
	err = trace.Start(f)
	if err != nil {
		panic(err)
	}
	//业务代码
    fmt.Println("hello trace")
	trace.Stop()
}

Web查看方式

1
$go tool trace trace.out

Debug查看

1
2
go build -o trace main.go
GODEBUG=schedtrace=1000 ./trace2

SCHED 调试的信息

0ms 从程序启动到输出经历的时间

gomaxprocs P的数量 ⼀般默认是和CPU的核⼼心数是⼀致的

idleprocs 处理理idle状态的P的数量,gomaxprocs-idleprocs= ⽬前正在执行的p的数量

threads 线程数量(包括M0,包括GODEBUG调试的线程)

spinningthreads 处于自旋状态的thread数量(自旋状态就是找到不G的M)

idlethread 处理idle状态的thread

runqueue 全局G队列中的G的数量

[0,0] 每个P的local queue本地队列列中,⽬目前存在G的数量量

Go调度器器GMP调度场景的全过程分析

P拥有G1,M1获取P后开始运行G1,G1使⽤用go func()创建了了G2,为了局部性G2优先加入到P1的本地队列。

G1运行完成后(函数:goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:schedule)。 从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute)。实现了线程M1的复用。

这三个场景是创建G后如何存放

规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执⾏。
假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为⾃旋线程(没有G但为运行状态的线程,不断寻找G)。

M2尝试从全局队列(简称“GQ”)取一批G放到P2的本地队列(函数:findrunnable())。M2从全局队列取的G数量符合下⾯面的公式: n = min(len(GQ)/GOMAXPROCS + 1, cap(GQ)/2)

全局队列已经没有G,那M就要执行work stealing(偷取):从其他有G的P那里偷取⼀半G过来,放到⾃己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中⼀半则只有1个G8,放到P2的本地队列并执行。

最多有GOMAXPROCS个自旋的线程(当前例子中的GOMAXPROCS=4,所以一共4个P),多余的没事做的线程会让他们休眠。

假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多就只能够存在4个P,所以P的数量应该永远是M>=P, 大部分都是M在抢占需要运行的P),G8创建了G9, G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列列有G、全局队列有G 或有空闲的M,P2都会⽴马唤醒1个M和它绑定,否则P2则会加⼊到空闲P列表,等待M来获取可用的P。 本场景中,P2本地队列列有G9,可以和其他空闲的线程M5绑定。

M2和P2会解绑,但M2会记住P2,然后G8和M2进⼊入系统调用状态。当G8和M2退出系统调用时,会尝试获取P2,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列,M2因为没有P的绑定而变成休眠状态(长时间休眠等待GC回收销毁)。

疑问

  1. 当M绑定P的本地队列中的G为空时,是先从全局队列中获取P还是从其他P的队列中偷取?
1
偷取
  1. 怎么理解 全局变量的G0是M0的G0 ?
1
意味着 Go 运行时最早创建的 G0 是 M0 的管理 Goroutine,它只负责调度和系统调用,不执行用户代码。
  1. GO调度完后放到哪里?
1
当 M 调度 Goroutine 时,G0 的作用是为 M 提供一个安全、独立的系统栈,让 M 可以执行调度操作和系统调用。调度结束后,M 会切换到新的 Goroutine 的栈,继续执行用户代码