# 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 的工作流程 🔄

  1. 创建 Goroutine 🆕

    • 程序通过 go 关键字创建新的 G
    • G 会被放入 P 的本地队列或全局队列
    • 如果本地队列已满,会放入全局队列
    • 创建 G 时会进行栈空间分配和初始化
  2. 调度过程

    • P 从本地队列获取 G
    • 将 G 交给 M 执行
    • 如果本地队列为空,会尝试从全局队列获取 G
    • 如果全局队列也为空,会尝试从其他 P"偷取"G
    • 每个 M 都有一个 G0,用于执行调度任务
    • G0 的栈空间大小是固定的,不会动态调整
    • G0 在整个程序的生命周期内只有一个
  3. 系统调用 🔄

    • 当 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 的优势 💪

  1. 高效的内存使用 💾

    • G 的栈大小动态调整
    • 避免固定栈大小带来的内存浪费
    • G0 使用固定栈,避免栈溢出
    • 支持栈的自动扩容和收缩
  2. 优秀的并发性能 🚀

    • 充分利用多核 CPU
    • 减少线程切换开销
    • 支持大量并发 Goroutine
    • 动态负载均衡
    • 高效的调度算法
  3. 灵活的调度策略 🎮

    • 支持工作窃取
    • 动态负载均衡
    • 避免线程饥饿
    • 抢占式调度
    • 支持协作式调度

# 实际应用建议 💡

  1. 合理设置 GOMAXPROCS ⚙️

    runtime.GOMAXPROCS(runtime.NumCPU())
  2. 避免创建过多 Goroutine ⚠️

    • 使用 goroutine 池
    • 控制并发数量
    • 注意内存使用
    • 避免 goroutine 泄漏
  3. 注意系统调用 🔄

    • 系统调用会阻塞 M
    • 考虑使用异步 IO
    • 避免频繁的系统调用
    • 使用 syscall.Syscall 替代 syscall.Syscall6
  4. 性能优化建议 🎯

    • 合理使用 goroutine
    • 避免 goroutine 泄漏
    • 使用 sync.Pool 复用对象
    • 注意锁的使用
    • 使用原子操作代替锁
    • 避免不必要的 goroutine 创建

# 总结 📝

GMP 模型是 Go 语言实现高并发的核心机制,它通过 Goroutine、Machine 和 Processor 三个组件的协同工作,实现了高效的并发调度。理解 GMP 模型对于编写高性能的 Go 程序至关重要!

记住:Go 的并发哲学是 "通过通信来共享内存,而不是通过共享内存来通信" 🎯

# 关键点回顾 🔍

  1. G0 是每个 M 的特殊 goroutine,用于调度
  2. M0 是主线程,负责程序初始化
  3. P0 是主处理器,负责执行 main goroutine
  4. 工作窃取机制确保负载均衡
  5. 抢占机制避免 goroutine 饥饿
  6. 系统调用时 M 会切换到 G0
  7. G0 使用固定栈,避免栈溢出
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

ZJM 微信支付

微信支付

ZJM 支付宝

支付宝