Golang 的 GMP 模型
一、gmp模型概念
如下图所示

1.1 g (Goroutine)
- g 是 goroutine 的缩写,是 Go 语言中对协程的抽象。它代表了一个可以被调度和执行的任务。
- G代表一个goroutine对象,每次go调用的时候,都会创建一个G对象。
- g 只有绑定到 p 上后,才能被调度执行。这意味着 g 需要一个处理器 p 来管理其执行。
- g的数量无硬编码限制,由可用内存决定。
实际约束:
每个 Goroutine 的初始栈大小约 2KB(可动态扩缩)。
假设内存无限,理论上可创建数十亿个 Goroutine。
实际场景中,受调度器性能和操作系统资源(如线程数、内存碎片)限制。
示例:
// 创建 100 万个 Goroutine(内存约 2GB) for i := 0; i < 1_000_000; i++ { go func() { select {} }() }
1.2 m (Machine)
- m 是 machine 的缩写,表示一个底层的操作系统线程,是 Go 语言中对线程的抽象。
- m 不能直接运行 g,要运行 g,必须先与 p 绑定。
- m 从 p 的本地队列中获取 g,如果 p 的 队列为空,m 会尝试从全局队列中取一批 g 放入 p 的本地队列,或者从其他 p 的本地队列中偷取一半的 g 放入自己 p 的本地队列中。
- 正因为 m 无需与 g 直接绑定,也不需要记录 g 的状态,因此 g 才能在不同的 m 上切换运行。
- m的数量默认无硬编码限制,但受以下因素约束:
阻塞系统调用:若 Goroutine 执行阻塞操作(如文件 I/O),Go 会创建新 M 来运行其他 G。
runtime.LockOSThread():强制绑定 Goroutine 到线程时,可能增加 M 的数量。系统限制:操作系统对线程数的限制(如 Linux 默认约 8K 线程)。
查看当前 M 的数量:
fmt.Println(runtime.NumCgoCall()) // 无关,需通过调试工具(如 `pprof`)查看。
1.3 p (Processor)
- p 是 processor 的缩写,是 Go 语言中的调度器。每一个运行的M都必须绑定一个P,就像线程必须在某一个CPU核上执行一样
- p 在 m 和 g 之间起到桥梁作用:对于 m 来说,只有绑定了 p 才能运行 g;而对于 g 来说,只有被 p 调度后才能执行。
- p 的数量决定了 g 的最大并行度,这个数量由 「GOMAXPROCS」 参数决定。硬编码上限为 256(无论是否设置
GOMAXPROCS超过此值)。
验证方法:
runtime.GOMAXPROCS(300) // 尝试设置超过 256 fmt.Println(runtime.GOMAXPROCS(0)) // 输出:256
二、gmp模型调度策略
gmp 模型的调度策略是 Go 语言中实现高效并发的关键,它确保了大量的 goroutines(g)能够在多个调度器(p)上有效地执行,同时利用底层操作系统线程(m)来最大化 CPU 的使用率。
全局G任务队列会和各个本地G任务队列按照一定的策略互相交换(如果本地队列满了,则把本地队列的一半送给全局队列)
P是用一个全局数组(255)来保存的,并且维护着一个全局的P空闲链表
2.1 调度器的工作机制
- 「协作式调度:」 Go 语言使用协作式调度机制,意味着 g 在某些特定点(如系统调用、I/O 操作)会主动让出 CPU,从而使调度器有机会调度其他 g 执行。这种机制减少了不必要的上下文切换,提高了调度效率。
- 「抢占式调度:」 为了防止某个 g 长时间占用 CPU 资源,Go 语言在 1.14 版本中引入了抢占式调度。如果一个 g 占用 CPU 时间过长,调度器会强制中断其执行,并将控制权交还给调度器,确保其他 g 能够得到执行机会。
2.2 gmp模型的工作流程
- 「新建goroutine:」 当新建一个 g 时,调度器会优先将其放入当前 p 的本地队列。如果本地队列已满,会将一半的 g 移动到全局队列中。
- 「优先本地队列调度:」 当一个 m 需要执行 g 时,优先从绑定的 p 的本地队列中获取 g 进行执行。这种方式达到无锁,提高调度效率。
- 「全局队列调度:」 如果 p 的本地队列为空,m 会尝试从全局队列中获取 g。访问全局队列需要加锁,所以相对较慢,但是能保证系统的负载均衡。
- 「工作窃取(Work Stealing):」 如果 m 从 p 的本地队列和全局队列都无法获取到 g,会尝试从其他 p 的本地队列中偷取一部分 g。这种机制能够避免某些 p 长时间空闲而其他 p 过载的情况。
- 「goroutine 的阻塞与唤醒(Hand Off):」 当 g 由于 I/O 操作或系统调用而阻塞时,m 会解除与 p 的绑定,并尝试从其他 p 获取 g 继续执行。当阻塞的 g 被唤醒时,调度器会将其重新分配到某个 p 的本地队列中,等待执行。
如果一个系统调用或者G任务执行太长,他就会一直占用这个线程,由于本地队列的G任务是顺序执行的,其它G任务就会阻塞了,怎样中止长任务的呢?
这样滴,启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:
-
记录所有P的G任务计数schedtick,(schedtick会在每执行一个G任务后递增)
-
如果检查到 schedtick一直没有递增,说明这个P一直在执行同一个G任务,如果超过一定的时间(10ms),就在这个G任务的栈信息里面加一个标记
-
然后这个G任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个G
-
如果没有遇到非内联函数(有时候正常的小函数会被优化成内联函数)调用的话,会一直执行这个G任务,直到它自己结束;如果是个死循环,并且GOMAXPROCS=1的话,恭喜你,夯住了!
对于一个G任务,中断后的恢复过程:
-
中断的时候将寄存器里的栈信息,保存到自己的G对象里面
-
当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。
三、性能优化建议优化
优化 Go 程序中的 Goroutine 使用以提高性能,可以从以下几个方面进行:
-
合理控制 Goroutine 数量:
- 过多的 Goroutine 会导致系统资源的过度消耗,甚至引发 Goroutine 泄露问题。因此,应根据实际需求合理控制 Goroutine 的数量,避免过度并发。
- 使用 Goroutine 池化技术可以减少 Goroutine 的创建和销毁开销,从而提高性能。
-
优化 Channel 的使用:
- Channel 是 Goroutine 之间通信的主要方式,但 Channel 的传递大数据会带来值拷贝的开销。因此,尽量避免在 Channel 中传递大数据。
- 使用 Channel 时,应尽量避免阻塞和死锁问题,确保 Channel 的使用高效且安全。
-
减少锁的使用:
- 锁是并发编程中常见的同步机制,但过度使用锁会导致性能下降。Go 推荐使用 Channel 的方式调用而不是共享内存,因为 Channel 之间存在大锁,可以降低锁的竞争力度。
- 无锁编程通过原子操作减少锁的使用,可以进一步提升并发性能。
-
使用 Context 控制 Goroutine 生命周期:
- 设置超时和使用
context.WithTimeout可以有效管理 Goroutine 的生命周期,避免 Goroutine 泄露。
- 设置超时和使用
-
减少系统调用:
- Goroutine 的实现是通过同步模拟异步操作,建议将同步调用隔离到可控 Goroutine 中,而不是直接高并发调用。
-
使用性能分析工具:
- 使用 Go 提供的性能分析工具如 pprof 和 trace,可以帮助检测 Goroutine 的运行时间、资源占用等,从而对 Goroutine 的创建和管理进行优化。
-
合理设置 GOMAXPROCS:
- GOMAXPROCS 控制了 Go 运行时可以使用的最大工作线程数。合理设置 GOMAXPROCS 可以提高并发性能。
- CPU 密集型任务:设为 CPU 核心数。
- I/O 密集型任务:适当增大(如 2~4 倍核心数)。
-
避免内存泄漏:
- 在使用切片和映射时要合理设置容量,避免内存泄漏。
注意:
goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换作下一个

浙公网安备 33010602011771号