--- tags: [go, golang, interview, gmp-questions] 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)。 ```mermaid 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: ```mermaid 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 ``` 1. **本地队列**(LRQ):无锁 CAS 操作 2. **全局队列**(GRQ):需要加锁 3. **网络轮询器**(netpoll):IO 就绪的 G 4. **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 是静态分配的。 **职责:** 1. 执行 Go 程序的启动流程(调度器、内存管理器、GC 器初始化) 2. 创建并运行第一个用户 goroutine(执行 main.main) 3. 程序退出时负责清理工作 --- ### Q9:g0 是什么? 🟡中等 ## 参考答案 g0 是每个 M 拥有的**特殊调度协程**,不是普通用户协程: | 特性 | g0 | 普通 goroutine | |------|----|--------------| | 栈空间 | 系统线程原始栈(8KB) | 可增长的分段栈(2KB 起) | | 用途 | 执行调度逻辑 | 执行业务代码 | | 创建方式 | 静态分配 | runtime.newg() | **核心作用:** 专门负责执行调度器代码(goroutine 创建/销毁/调度决策)、垃圾回收、栈扫描、信号处理等运行时操作。 > [!warning] ⚠️ 高频陷阱 > 为什么需要 g0?因为调度器不能在普通 goroutine 的栈上执行——会有栈空间冲突和递归调度的问题。g0 提供了独立的执行环境。 --- ### Q10:g0 栈和用户栈如何切换? 🟡中等 ## 参考答案 本质是 **SP 寄存器和栈指针的切换**: 1. **用户 -> g0**:通过 `mcall()` 保存当前 PC/SP 到 gobuf,将 SP 指向 g0 的栈 2. **g0 -> 用户**:通过 `gogo()` 恢复用户 goroutine 保存的寄存器状态 这些切换在汇编文件中实现(`runtime·mcall` 和 `runtime·gogo`),直接操作 CPU 寄存器,确保原子性和高效性。 > [!info] 🔗 延伸阅读 > - [[hzh/GolangStar/Go语言原理/gmp调度原理]] — 栈切换的汇编实现细节 ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Goroutine]] - [[hzh/GolangStar/Go语言原理/gmp调度原理]] - [[hzh/GolangStar/Go面试题库/内存管理面试题]]