7.5 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-06-07 14:30 |
GMP 面试题 ⚙️
概述
本文件涵盖 Go GMP 调度模型的 10 道高频面试题,涉及 G/M/P 含义、调度策略、抢占机制、工作窃取等核心概念。GMP 是 Go 高并发的基石,深入理解它是面试中冲击高薪的关键。
关联笔记
- hzh/GolangStar/Go语言进阶/Goroutine — Goroutine 基础
- hzh/GolangStar/Go语言原理/gmp调度原理 — GMP 源码级详解
- hzh/GolangStar/Go面试题库/内存管理面试题 — 内存与调度的关系
正文
Q1:Go 语言的 GMP 模型是什么? 🟡中等
[!question] ❓ 思考一下 如果让你设计一个能调度百万级协程的系统,你会怎么组织 M(系统线程)和 G(协程)的关系?直接 M-G 一对一行吗?
参考答案
GMP 是 Go 运行时的核心调度模型:
| 组件 | 含义 | 角色 |
|---|---|---|
| G (Goroutine) | 协程 | 干活的工作单元 |
| M (Machine) | 系统线程 | 真正执行代码的"手" |
| P (Processor) | 逻辑处理器 | G 和 M 之间的"桥梁",负责调度 G |
调度逻辑: M 必须绑定 P 才能执行 G。每个 P 维护自己的本地队列(最多 256 个 G),M 从 P 的本地队列取 G 执行。当本地队列空时,M 按优先级从全局队列、网络轮询器或其他 P 中窃取 goroutine(work-stealing)。
graph LR
G1["G1"] --> P["P (逻辑处理器)"]
G2["G2"] --> P
G3["G3"] --> P
G4["G4"] --> P
P --> M["M (系统线程)"]
style P fill:#FF9800,color:#fff
style M fill:#2196F3,color:#fff
[!note] 📝 核心考点 GMP 的核心价值:用少量 M(系统线程)调度海量 G(协程)。P 层实现了无锁本地调度,避免了全局锁竞争。
Q2:什么是 Go scheduler? 🟢简单
参考答案
Go scheduler 是 Go 运行时的协程调度器,内嵌在程序中一起运行。它的核心工作是决定哪个 goroutine 在哪个线程上运行,以及何时进行上下文切换。
核心函数 schedule() 在无限循环中寻找可运行的 goroutine,找到后通过 execute() 切换到 goroutine 执行。
[!info] 🔗 延伸阅读
- hzh/GolangStar/Go语言原理/gmp调度原理 — schedule() 源码分析
Q3:Go 的 goroutine 抢占调度策略? 🟡中等
参考答案
Go 采用抢占式调度,由 sysmon 线程监控。不同版本的实现方式不同:
Go 1.14 之前:协作式抢占
- 编译器在函数调用入口插入检查代码
- G 进行函数调用时检查
preempt标志 - 缺陷:不含函数调用的超大循环不会被抢占,造成调度延迟
Go 1.14 及之后:基于信号的异步抢占
- sysmon 检测到运行超过 10ms 的 G
- 向 M 发送 SIGURG 信号
- gsignal goroutine 处理信号,停止正在运行的 G
[!warning] ⚠️ 高频陷阱 Go 1.14 之前的协作式抢占无法处理"纯计算密集型且无函数调用的 goroutine",这是最常见的面试追问点。
Q4:发生调度的时机有哪些? 🟢简单
参考答案
| 触发条件 | 说明 |
|---|---|
| Channel 操作 | 等待读写未缓冲 channel |
| time.Sleep() | 等待睡眠时间结束 |
| Mutex 等待 | 获取互斥量失败 |
| 系统调用 | 发起 syscall 进入内核态 |
| 主动让出 | runtime.Gosched() |
| 抢占 | sysmon 强制抢占(Go 1.14+) |
Q5:M 寻找可运行 G 的过程? 🟡中等
参考答案
M 按以下优先级顺序寻找 G:
graph TD
A[M 需要执行 G] --> B{本地队列 LRQ?}
B -->|有| C[runqget 取出]
B -->|无| D{全局队列 GRQ?}
D -->|有| E[globrunqget 取一半]
D -->|无| F{网络轮询 netpoll?}
F -->|有| G[取出就绪的 G]
F -->|无| H[从其他 P 偷 work-steal]
H --> I[随机找一个 P 偷一半]
style C fill:#4CAF50,color:#fff
style E fill:#2196F3,color:#fff
style G fill:#9C27B0,color:#fff
style I fill:#FF9800,color:#fff
- 本地队列(LRQ):无锁 CAS 操作
- 全局队列(GRQ):需要加锁
- 网络轮询器(netpoll):IO 就绪的 G
- Work-stealing:随机找其他 P 偷一半
[!tip] 💡 面试技巧 "Go 的调度器就像一个聪明的餐厅经理:先看自己桌有没有客人(本地队列),没有就去公共排队区(全局队列),再去门口看看有没有新到的人(netpoll),最后实在没人就从隔壁桌'借'几个客人来(steal)。"
Q6:GMP 能不能去掉 P 层? 🟡中等
参考答案
理论上可以,但会带来严重的性能问题。
| 方案 | 问题 |
|---|---|
| 去掉 P(变成 GM) | 所有 M 争抢全局锁,高并发下锁竞争严重 |
| 保留 P | 每个 P 独立本地队列,大部分情况无需全局锁 |
[!note] 📝 核心考点 P 的价值在于实现了无锁的本地调度。M 绑定 P 后直接从本地队列取 G,只有队列空了才去偷取,大大减少了锁竞争。
Q7:P 和 M 在什么时候被创建? 🟡中等
参考答案
| 组件 | 创建时机 | 说明 |
|---|---|---|
| P | 初始化时一次性创建 | schedinit() → procresize(),数量为 GOMAXPROCS |
| M | 按需创建 | 初始只有 m0,阻塞场景或 startm() 触发 |
M 的创建条件:
- 所有现有 M 都在执行系统调用,但还有可运行的 G
- 没有空闲 M 可以绑定 P
[!info] 🔗 延伸阅读
- hzh/GolangStar/Go语言原理/gmp调度原理 — schedinit / procresize 源码分析
Q8:m0 是什么? 🟢简单
参考答案
m0 是 Go 启动时创建的第一个 M,对应主系统线程,贯穿整个生命周期。与其他动态创建的 M 不同,m0 是静态分配的。
职责:
- 执行 Go 程序的启动流程(调度器、内存管理器、GC 器初始化)
- 创建并运行第一个用户 goroutine(执行 main.main)
- 程序退出时负责清理工作
Q9:g0 是什么? 🟡中等
参考答案
g0 是每个 M 拥有的特殊调度协程,不是普通用户协程:
| 特性 | g0 | 普通 goroutine |
|---|---|---|
| 栈空间 | 系统线程原始栈(8KB) | 可增长的分段栈(2KB 起) |
| 用途 | 执行调度逻辑 | 执行业务代码 |
| 创建方式 | 静态分配 | runtime.newg() |
核心作用: 专门负责执行调度器代码(goroutine 创建/销毁/调度决策)、垃圾回收、栈扫描、信号处理等运行时操作。
[!warning] ⚠️ 高频陷阱 为什么需要 g0?因为调度器不能在普通 goroutine 的栈上执行——会有栈空间冲突和递归调度的问题。g0 提供了独立的执行环境。
Q10:g0 栈和用户栈如何切换? 🟡中等
参考答案
本质是 SP 寄存器和栈指针的切换:
- 用户 -> g0:通过
mcall()保存当前 PC/SP 到 gobuf,将 SP 指向 g0 的栈 - g0 -> 用户:通过
gogo()恢复用户 goroutine 保存的寄存器状态
这些切换在汇编文件中实现(runtime·mcall 和 runtime·gogo),直接操作 CPU 寄存器,确保原子性和高效性。
[!info] 🔗 延伸阅读
- hzh/GolangStar/Go语言原理/gmp调度原理 — 栈切换的汇编实现细节