# Go 语言 GMP 模型详解 🚀
https://mp.weixin.qq.com/s/jIWe3nMP6yiuXeBQgmePDg
# 什么是 GMP? 🤔
GMP 是 Go 语言运行时 (runtime) 的调度器,它负责将 goroutine 调度到操作系统线程上执行。这个调度器是 Go 语言实现高并发的重要基石!
# GMP 模型的核心组件 🎯
# 1. G (Goroutine) 🏃♂️
- 轻量级的用户态线程
- 包含执行栈、调度信息、等待队列等
- 初始栈大小只有 2KB,但可以动态扩容
- 由 Go 程序通过
go
关键字创建 - 每个 G 都有自己的执行上下文和栈空间
- 状态包括:_Gidle、_Grunnable、_Grunning、_Gsyscall、_Gwaiting、_Gdead
- 可以被抢占,支持协作式调度
# 2. M (Machine) ⚙️
- 代表操作系统线程
- 由操作系统调度
- 每个 M 都有一个特殊的 G0(调度协程)
- 数量由
GOMAXPROCS
环境变量决定 - M0 是启动程序后的编号为 0 的线程,负责执行初始化操作和启动第一个 G
- M0 在全局变量 runtime.m0 中,不需要在 heap 上分配
- M0 负责执行初始化操作和启动第一个 G(main goroutine)
- M0 还可以负责执行垃圾回收等操作
# 3. P (Processor) 🎮
- 调度上下文,可以看作一个 "处理器"
- 维护一个本地 G 队列
- 数量通常等于 CPU 核心数
- 负责将 G 调度到 M 上执行
- P0 是启动程序后的编号为 0 的 P,负责执行 main goroutine
- 每个 P 都有一个本地队列,用于存储等待执行的 G
- P 的数量可以通过 GOMAXPROCS 环境变量或 runtime.GOMAXPROCS () 函数设置
# GMP 的工作流程 🔄
创建 Goroutine 🆕
- 程序通过
go
关键字创建新的 G - G 会被放入 P 的本地队列或全局队列
- 如果本地队列已满,会放入全局队列
- 创建 G 时会进行栈空间分配和初始化
- 程序通过
调度过程 ⚡
- P 从本地队列获取 G
- 将 G 交给 M 执行
- 如果本地队列为空,会尝试从全局队列获取 G
- 如果全局队列也为空,会尝试从其他 P"偷取"G
- 每个 M 都有一个 G0,用于执行调度任务
- G0 的栈空间大小是固定的,不会动态调整
- G0 在整个程序的生命周期内只有一个
系统调用 🔄
- 当 G 进行系统调用时,M 会释放 P
- P 会被分配给其他空闲的 M
- 系统调用结束后,G 会重新进入队列等待调度
- 使用 hand off 机制,避免线程阻塞
- 系统调用期间,M 会切换到 G0 执行调度任务
# GMP 的调度策略 🎯
# 1. 工作窃取(Work Stealing)🦹♂️
- 当 P 的本地队列为空时
- 会随机选择一个 P,尝试从其本地队列 "偷取" 一半的 G
- 这种机制可以平衡各个 P 的工作负载
- 避免资源浪费和线程饥饿
- 窃取时会考虑负载均衡
# 2. 自旋线程(Spinning Threads)⚡
- 部分 M 会处于自旋状态
- 自旋的 M 会不断寻找可执行的 G
- 避免频繁的线程创建和销毁
- 提高响应速度
- 自旋线程数量由 GOMAXPROCS 决定
# 3. 全局队列(Global Queue)🌍
- 存放等待调度的 G
- 当 P 的本地队列满时,会将 G 放入全局队列
- 优先级低于本地队列
- 用于负载均衡
- 全局队列的访问需要加锁
# 4. 抢占机制(Preemption)⚡
- sysmon 线程监控 goroutine 运行时间
- 超过阈值时触发抢占
- 通过 timer 实现定时抢占
- 避免 goroutine 长时间占用资源
- 支持协作式调度和抢占式调度
# GMP 的优势 💪
高效的内存使用 💾
- G 的栈大小动态调整
- 避免固定栈大小带来的内存浪费
- G0 使用固定栈,避免栈溢出
- 支持栈的自动扩容和收缩
优秀的并发性能 🚀
- 充分利用多核 CPU
- 减少线程切换开销
- 支持大量并发 Goroutine
- 动态负载均衡
- 高效的调度算法
灵活的调度策略 🎮
- 支持工作窃取
- 动态负载均衡
- 避免线程饥饿
- 抢占式调度
- 支持协作式调度
# 实际应用建议 💡
合理设置 GOMAXPROCS ⚙️
runtime.GOMAXPROCS(runtime.NumCPU())
避免创建过多 Goroutine ⚠️
- 使用 goroutine 池
- 控制并发数量
- 注意内存使用
- 避免 goroutine 泄漏
注意系统调用 🔄
- 系统调用会阻塞 M
- 考虑使用异步 IO
- 避免频繁的系统调用
- 使用 syscall.Syscall 替代 syscall.Syscall6
性能优化建议 🎯
- 合理使用 goroutine
- 避免 goroutine 泄漏
- 使用 sync.Pool 复用对象
- 注意锁的使用
- 使用原子操作代替锁
- 避免不必要的 goroutine 创建
# 总结 📝
GMP 模型是 Go 语言实现高并发的核心机制,它通过 Goroutine、Machine 和 Processor 三个组件的协同工作,实现了高效的并发调度。理解 GMP 模型对于编写高性能的 Go 程序至关重要!
记住:Go 的并发哲学是 "通过通信来共享内存,而不是通过共享内存来通信" 🎯
# 关键点回顾 🔍
- G0 是每个 M 的特殊 goroutine,用于调度
- M0 是主线程,负责程序初始化
- P0 是主处理器,负责执行 main goroutine
- 工作窃取机制确保负载均衡
- 抢占机制避免 goroutine 饥饿
- 系统调用时 M 会切换到 G0
- G0 使用固定栈,避免栈溢出