news 2026/6/14 23:25:15

Go语言Goroutine调度器GMP模型深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go语言Goroutine调度器GMP模型深度解析

Go语言Goroutine调度器GMP模型深度解析

从源码到实战,彻底搞懂Go调度器的工作原理


前言

很多Go开发者写了几年代码,对Goroutine的理解还停留在"轻量级线程"这个层面。面试被问到GMP模型时,只能说出G是协程、M是线程、P是处理器,但具体怎么调度的?为什么Goroutine比线程轻量?什么时候会发生抢占?网络I/O阻塞时调度器怎么处理?

这些问题答不上来,说明对Go调度器的理解还远远不够。本文将从源码层面深入剖析GMP调度模型,结合实际踩坑经验,帮你彻底搞懂Go调度器。


一、为什么需要GMP模型

1.1 从最朴素的调度说起

最简单的协程调度模型是N:1——N个协程绑定到1个OS线程上:

协程A → 协程B → 协程C → 协程A → ... ↑ 1个OS线程

这个模型的问题很明显:如果协程A调用了阻塞系统调用(比如文件I/O),整个线程被阻塞,其他协程全部卡住。

1.2 M:N调度的挑战

理想的方案是M:N——M个协程分布在N个OS线程上并行执行:

协程A 协程B 协程C 协程D 协程E 协程F ↓ ↓ ↓ ↓ ↓ ↓ 线程1 线程2 线程1 线程3 线程2 线程1

但M:N调度面临几个核心问题:

  1. 线程创建开销大:每个协程都创建一个线程,退化成1:1模型
  2. 上下文切换成本高:线程切换需要内核态参与,保存/恢复寄存器
  3. 资源竞争:多个线程同时调度协程,需要加锁保护
  4. 局部性丢失:协程可能被调度到不同的CPU核心,缓存失效

Go的GMP模型就是为了解决这些问题而设计的。

1.3 GMP模型的核心思想

┌─────────────────────────────────────────────────┐ │ Go Runtime │ │ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ G1 │ │ G2 │ │ G3 │ │ G4 │ ... │ │ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │ │ └─────────┴─────────┴─────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────┐ │ │ │ 全局队列 (Global Queue) │ │ │ └──────────────────┬───────────────────┘ │ │ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ P0 │ │ P1 │ │ P2 │ │ │ │ 本地队列 │ │ 本地队列 │ │ 本地队列 │ │ │ │ [G5,G6] │ │ [G7,G8] │ │ [G9] │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ ↓ ↓ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ M0 │ │ M1 │ │ M2 │ │ │ │ (OS线程) │ │ (OS线程) │ │ (OS线程) │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────┘

G(Goroutine):轻量级协程,包含栈、程序计数器、状态等
M(Machine):OS线程,真正执行代码的实体
P(Processor):逻辑处理器,持有本地运行队列,数量等于GOMAXPROCS

关键关系:

  • P的数量由GOMAXPROCS决定(默认等于CPU核心数)
  • M的数量可以远大于P(M会按需创建)
  • G必须绑定到P上才能被M执行
  • P同一时刻只能绑定一个M

二、GMP三组件详解

2.1 G(Goroutine)

每个Goroutine在runtime中对应一个g结构体:

// runtime/runtime2.gotypegstruct{stack stack// 栈空间 [stack.lo, stack.hi)stackguard0uintptr// 栈溢出检测地址stackguard1uintptr// C栈溢出检测地址_panic*_panic// panic链表_defer*_defer// defer链表m*m// 当前绑定的Msched gobuf// 调度信息(保存/恢复用)atomicstatusuint32// 状态:_Gidle/_Grunnable/_Grunning/_Gsyscall/_Gwaiting/_Gdeadgoidint64// goroutine IDschedlink guintptr// 链表指针(用于本地队列)preemptbool// 抢占标记// ...}typegobufstruct{spuintptr// 栈指针pcuintptr// 程序计数器g guintptr// 关联的gctxt unsafe.Pointer retuintptrlruintptrbpuintptr}

Goroutine的初始栈大小:2KB(Go 1.4+),可动态增长到最大1GB。
对比OS线程默认栈大小8MB,这就是Goroutine轻量的核心原因之一。

// 创建一个goroutine只需要分配2KB栈空间// 创建一个OS线程需要分配8MB栈空间// 也就是说,同样内存可以创建:// 1GB / 2KB = 524,288 个goroutine// 1GB / 8MB = 128 个OS线程

2.2 M(Machine)

M代表OS线程,结构体定义:

typemstruct{g0*g// 特殊的goroutine,用于执行调度代码curg*g// 当前正在运行的goroutinep puintptr// 绑定的Pnextp puintptr// 预绑定的Poldp puintptr// 之前的P(用于handoff)idint64spinningbool// 是否正在寻找workblockedbool// 是否阻塞在系统调用上// ...}

M的几个关键特性

  1. M的数量可以远大于P:当M阻塞在系统调用上时,Go会创建新的M来保持P的利用率
  2. M的创建有上限:默认最多10000个(可通过debug.SetMaxThreads调整)
  3. M0:进程启动时创建的第一个M,负责执行初始化代码和全局调度
  4. 每个M都有一个特殊的g0:用于执行runtime调度代码,不占用用户goroutine的栈

2.3 P(Processor)

P是GMP模型的核心调度单元:

typepstruct{idint32statusuint32// _Pidle/_Prunning/_Psyscall/_Pgcstop/_Pdeadm muintptr// 绑定的M// 本地运行队列runqheaduint32runqtailuint32runq[256]guintptr// 环形队列,最多256个Grunnext guintptr// 下一个优先执行的G(用于减少调度延迟)// 全局队列缓存gFreestruct{gList nint32}// ...}

P的本地队列

  • 固定大小256的环形队列
  • 当本地队列满时,会将一半G转移到全局队列
  • 本地队列不需要加锁(只有绑定的M能访问),效率极高

P的状态流转

_Pidle → _Prunning → _Psyscall → _Prunning → _Pgcstop → _Pdead ↑ ↓ └────── _Pidle ────────┘

三、调度流程详解

3.1 Goroutine的创建

当你写go func()时,runtime会执行newproc()

// runtime/proc.gofuncnewproc(sizint32,fn*funcval){argp:=add(unsafe.Pointer(&fn),sys.PtrSize)gp:=getg()pc:=getcallerpc()systemstack(func(){newg:=gfget(gp.m.p)// 从P的空闲G链表获取ifnewg==nil{newg=malg(_StackMin)// 创建新G,分配2KB栈casgstatus(newg,_Gidle,_Grunnable)}// 保存函数参数到G的栈totalSize:=4*sys.RegSize+uintptr(siz)totalSize+=-totalSize&15// 对齐sp:=newg.stack.hi-totalSize// ...newg.sched.pc=funcPC(goexit)+sys.PCQuantum newg.sched.sp=sp// 将G放入本地队列或全局队列runqput(gp.m.p.ptr(),newg,true)// 如果有空闲P且没有M在运行,唤醒新的Mifatomic.Load(&sched.npidle)!=0&&atomic.Load(&sched.nmspinning)==0{wakep()}})}

关键步骤

  1. 从P的空闲链表获取G(复用已销毁的G,避免频繁分配)
  2. 如果没有空闲G,创建新G并分配2KB栈
  3. 将G放入P的本地队列(runqput
  4. 如果有空闲P,唤醒新的M来执行

3.2 调度主循环

调度器的核心是schedule()函数:

// runtime/proc.gofuncschedule(){gp:=getg()// 1. 检查GC标记ifgp.m.preemptoff!=""{// ...}top:varpp*pifsched.gcwaiting!=0{// 等待GC完成stopm()gototop}// 2. 检查是否有GC标记的G需要执行ifgcBlackenEnabled!=0&&gp.m.curg!=nil
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/13 6:21:08

STM32F103 IAP实战:从Bootloader设计到远程固件更新

1. 为什么你的STM32需要IAP升级? 第一次接触IAP(In-Application Programming)这个概念时,我正蹲在工厂车间的设备旁边,手里拿着需要升级的STM32板子发愁。产线上30台设备需要更新程序,而每台设备都要拆外壳…

作者头像 李华
网站建设 2026/5/13 6:19:20

创梦汤锅学习日记day1

今天是2026年5月12日,很久没有发布过博客了,从今天开始,我将先开启长达3个月的从技术到项目落地到综合扩展实践的发展路线。我将总结自己的学习经历,发布自己的心得。从技术上,目前主要走游戏开发方向,基于…

作者头像 李华
网站建设 2026/5/13 6:12:13

阴阳师百鬼夜行AI自动化:3分钟配置实现全智能碎片收集

阴阳师百鬼夜行AI自动化:3分钟配置实现全智能碎片收集 【免费下载链接】OnmyojiAutoScript Onmyoji Auto Script | 阴阳师脚本 项目地址: https://gitcode.com/gh_mirrors/on/OnmyojiAutoScript 还在为手动刷百鬼夜行而烦恼吗?每天重复点击、熬夜…

作者头像 李华
网站建设 2026/5/13 6:07:06

阿里巴巴DeepResearch框架:NLP研究工具箱的模块化设计与实战应用

1. 项目概述:从标题“Alibaba-NLP/DeepResearch”我们能读出什么?看到“Alibaba-NLP/DeepResearch”这个项目标题,我的第一反应是,这大概率不是一个面向普通用户的“开箱即用”的应用,而是一个来自阿里巴巴自然语言处理…

作者头像 李华
网站建设 2026/5/13 6:04:33

终极指南:如何在Windows电脑上直接安装和运行安卓应用

终极指南:如何在Windows电脑上直接安装和运行安卓应用 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 还在为在电脑上运行安卓应用而烦恼吗?你是…

作者头像 李华