Go语言之所以能在高并发领域脱颖而出,核心优势之一就是轻量化的并发调度机制 。不同于Java、C++依赖操作系统内核线程实现并发的方式,Go语言在用户态实现了一套自研调度器------GMP调度模型。
正是依托GMP模型,Go可以轻松支撑数十万、上百万级别的并发协程(Goroutine),且内存占用极低、调度开销极小。本文将从零拆解GMP模型的核心组件、调度流程、核心机制、阻塞场景及面试高频考点,帮你彻底吃透Go并发底层原理。
GMP是Go Runtime实现的用户态调度模型 ,由三个核心组件构成:G(Goroutine)、M(Machine)、P(Processor) 。三者协同工作,实现了用户态协程与内核线程的解耦,规避了操作系统线程调度的高额开销。
先明确核心定位:G是任务、M是执行载体、P是调度上下文,三者缺一不可。
G即Go协程,是用户创建的最小并发执行单元,也是GMP模型中的被调度对象 ,对应代码中 go func() 启动的任务。
核心概念
1.1 G(Goroutine):最小并发任务单元
G的核心特性
- 轻量化:初始栈仅2KB,可动态扩容、缩容,单个进程可创建上百万个G,远优于操作系统线程(默认栈MB级别,上限数千)。
- 用户态管理:由Go Runtime调度,不直接绑定内核线程,创建、销毁、切换开销极低。
- 状态丰富:包含运行、就绪、阻塞、死亡等状态,调度器根据状态流转实现任务切换。
- 资源独立:每个G拥有独立的栈空间、程序计数器、寄存器上下文,保证任务独立执行。
简单理解:G就是待执行的业务任务,本身不具备执行能力,必须依托M和P才能运行。
1.2 M(Machine):内核执行线程
M对应操作系统的内核线程,是真正执行机器指令的载体,负责执行G的代码逻辑。
M的核心特性
- 内核级线程:由操作系统调度管理,拥有内核线程的完整权限。
- 无调度能力:M本身不负责任务调度,只负责"干活",必须绑定P才能执行G。
- 动态伸缩:Go Runtime会根据任务阻塞情况动态创建、销毁M,默认M数量与CPU核心数匹配,阻塞时会新建M保证并发。
核心关系:M是苦力,只负责执行;没有P的M,无法执行任何G任务。
1.3 P(Processor):调度上下文与资源管理者
P是GMP模型的核心调度枢纽,是Go调度器的核心设计,也是Go高并发高效的关键。P本质是一个用户态的调度上下文,承接G和M的桥梁。
P的核心特性
- 固定数量 :P的数量默认等于CPU核心数(GOMAXPROCS),可手动配置,代表程序最大并行执行的G数量。
- 资源载体:持有运行G所需的全部资源,包括本地G队列、内存缓存(MCache)、调度状态等。
- 绑定关系:同一时刻一个P只能绑定一个M,一个M同一时刻只能绑定一个P。
- 负载均衡:通过本地队列、全局队列、工作窃取机制,平衡各个P的任务负载。
简单总结三者关系:P管理任务队列,M绑定P获取任务,M执行P分配的G任务。
底层结构
2.1 G结构体(Goroutine 底层结构)
G是协程的底层数据结构,存储单个协程的运行状态、栈信息、寄存器、阻塞标记,是调度器的最小调度单元。
Go
// runtime/runtime2.go
type g struct {
// 栈内存信息:栈的起始地址、结束地址、栈扩容标记
stack stack
stackguard0 uintptr
// 协程状态:running/ready/waiting/dead等
status uint32
// 上下文寄存器:线程切换时保存/恢复现场
sched gobuf
// 阻塞相关:阻塞原因、唤醒信号
waitreason uint8
park bool
// 所属M、绑定的P(运行时绑定)
m *m
p *p
// 抢占调度标记
preempt bool
preemptoff uintptr
// 协程唯一ID
goid uint64
}
G核心字段含义:
- status :协程状态机,核心状态包含
_Grunning(运行)、_Gready(就绪)、_Gwaiting(阻塞)、_Gdead(消亡),调度器完全依靠该字段判断G是否可执行。 - stack:动态栈结构体,记录栈起止地址,支撑Go 2KB初始栈、动态扩容/缩容特性,是G轻量化的核心。
- sched(gobuf) :保存PC程序计数器、SP栈指针、寄存器上下文,G切换时保存现场,恢复执行时读取现场,实现用户态无内核切换调度。
- m/p:记录当前G绑定的M和P,只有处于Running状态的G,才会绑定有效M、P。
- preempt:抢占标记,Go1.14异步抢占核心字段,标记当前G是否需要被强制让出CPU。
2.2 M结构体(Machine 内核线程结构)
M对应操作系统内核线程,负责执行G的机器指令,存储线程信息、绑定P、阻塞状态、调度缓存,是真正的执行载体。
go
// runtime/runtime2.go
type m struct {
// 绑定的P:同一时刻一个M仅绑定一个P
p *p
oldp *p
// 当前正在执行的G
curg *g
// 线程ID,对应系统内核线程
tid uint64
// 阻塞标记、系统调用标记
blocked bool
incgo bool
// 调度栈、系统栈
g0 *g
mcache *mcache
}
M核心字段含义:
- p/oldp:当前绑定的P、解绑前的旧P。系统调用阻塞时,M会解绑P存入oldp,唤醒后尝试抢回原P。
- curg:M当前正在执行的G,非空则代表M处于工作状态。
- g0:M的调度栈G,每个M独有,用于执行调度逻辑、栈切换、系统调用,区别于用户G的业务栈。
- mcache:线程级内存缓存,跟随M绑定P使用,加速小对象内存分配。
- tid:操作系统内核线程ID,证明M是真实内核线程。
2.3 P结构体(Processor 调度上下文结构)
P是GMP调度的核心枢纽结构体,维护任务队列、调度状态、内存资源,是实现用户态调度、无锁队列、工作窃取的核心。
go
// runtime/runtime2.go
type p struct {
// P的唯一ID,0~GOMAXPROCS-1
id int32
// P的运行状态:空闲/运行/抢占/停止
status uint32
// 高优先级临时G(runnext优先级最高)
runnext *g
// 本地G环形队列,容量256,无锁调度
runq [256]guintptr
runqhead uint32
runqtail uint32
// 全局内存缓存,P独有,无锁分配
mcache *mcache
// 调度时间片、抢占计时器
tick int32
// 工作窃取、负载均衡标记
stealSeq uint32
}
P核心字段含义:
- status :P状态机,包含
_Prunning、_Pidle、_Pstopped,决定当前P是否可参与调度。 - runnext:最高优先级G插槽,新创建的G优先放入此处,优先调度,提升局部执行效率。
- runq:本地环形任务队列(固定256容量),无锁操作,是GMP高性能的关键,绝大多数G优先在本地队列调度。
- mcache:P独享内存缓存,多P之间无锁竞争,极大提升内存分配效率。
- stealSeq:工作窃取序列号,用于协调跨P窃取任务,避免重复窃取、竞争冲突,保障负载均衡安全。
GMP执行过程

执行流程
1、开始一个Goroutine,先查看P的本地队列是否已满,有空间进入本地队列,没有空间进入全局队列,P的个数由环境变量GOMAXPROCS,runtime.GOMAXPROCS(),默认是和CPU数量一致,P队列中G的数量最多不超过256个。
2、M1优先绑定P1的本地队列中去获取Goroutine,如果本地队列P里面没有可执行的队列的G就会从全局队列中获取可执行的G,如果全局队列也没有可执行的G,他就会从其他的GMP组合里去窃取可执行的G,正常情况下,会从其他P的本地队列里窃取一半的可执行的G来运行。
3、如果发生系统调用阻塞被其他M接管,系统调用结束放入休眠队列。
M的最大数量是10000,可通过debug.SetMaxThreads函数去修改
GMP调度模型的一些细节:
- 尽可能复用系统线程M,避免频繁的线程创建和销毁,利用多核并行能力,让同时处理的任务队列数量等于CPU的核数。
- 任务窃取机制,M可以从其他M绑定的P的运行队列偷取G执行。
- Hand Off交接机制,M阻塞时会将M上P的运行队列交给其他M执行,基于协作的抢占机制。
- Go1.14基于信号的抢占式调度解决了GC和栈扫描时无法被抢占问题。
扩展
1、Gorutine的调度时机出现在哪些情况? 主动调度、被动调度、抢占调度。
主动调度:通过代码中执行runtime.Gosched进行主动抢占,原理是先切g0,G与M解绑,G入全局队列。
被动调度:协程在休眠、阻塞、执行垃圾回收而暂停时让出执行机会,被动调度的目的是保证最大化利用CPU的资源,先切g0,G与M解绑,G不入全局队列。
抢占调度:系统监控定时检测,运行时间过长或系统调用的协程被抢占。
2、M如何等待唤醒?
- 选择让M进入阻塞状态,通过线程内核态与用户态切换唤醒
- 让M自旋,CPU空转等待可执行的G
- 自旋的线程数不会超过
GOMAXPROCS
3、工作窃取与工作共享区别?
- 工作共享是处理器主动将将一部分线程迁移到其他的处理器上执行
- 工作窃取是未被充分利用的处理器会主动寻找其他处理器上的线程