Files
cs-note/hzh/GolangStar/Go面试题库/GMP面试题.md
T

224 lines
7.5 KiB
Markdown
Raw Normal View History

2026-06-07 11:08:10 +08:00
---
2026-06-07 12:14:39 +08:00
tags: [go, golang, interview, gmp-questions]
create time: 2026-06-07 14:30
2026-06-07 11:08:10 +08:00
---
2026-06-07 12:14:39 +08:00
# GMP 面试题 ⚙️
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 概述
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
本文件涵盖 Go GMP 调度模型的 10 道高频面试题,涉及 G/M/P 含义、调度策略、抢占机制、工作窃取等核心概念。GMP 是 Go 高并发的基石,深入理解它是面试中冲击高薪的关键。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 关联笔记
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
- [[hzh/GolangStar/Go语言进阶/Goroutine]] — Goroutine 基础
- [[hzh/GolangStar/Go语言原理/gmp调度原理]] — GMP 源码级详解
- [[hzh/GolangStar/Go面试题库/内存管理面试题]] — 内存与调度的关系
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 正文
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### Q1:Go 语言的 GMP 模型是什么? 🟡中等
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!question] ❓ 思考一下
> 如果让你设计一个能调度百万级协程的系统,你会怎么组织 M(系统线程)和 G(协程)的关系?直接 M-G 一对一行吗?
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 参考答案
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
GMP 是 Go 运行时的**核心调度模型**:
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
| 组件 | 含义 | 角色 |
|------|------|------|
| **G (Goroutine)** | 协程 | 干活的工作单元 |
| **M (Machine)** | 系统线程 | 真正执行代码的"手" |
| **P (Processor)** | 逻辑处理器 | G 和 M 之间的"桥梁",负责调度 G |
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
**调度逻辑:** M 必须绑定 P 才能执行 G。每个 P 维护自己的本地队列(最多 256 个 G),M 从 P 的本地队列取 G 执行。当本地队列空时,M 按优先级从全局队列、网络轮询器或其他 P 中窃取 goroutine(work-stealing)。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
```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
```
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!note] 📝 核心考点
> GMP 的核心价值:**用少量 M(系统线程)调度海量 G(协程)**。P 层实现了无锁本地调度,避免了全局锁竞争。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
---
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### Q2:什么是 Go scheduler? 🟢简单
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 参考答案
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
Go scheduler 是 Go 运行时的**协程调度器**,内嵌在程序中一起运行。它的核心工作是决定哪个 goroutine 在哪个线程上运行,以及何时进行上下文切换。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
核心函数 `schedule()` 在无限循环中寻找可运行的 goroutine,找到后通过 `execute()` 切换到 goroutine 执行。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!info] 🔗 延伸阅读
> - [[hzh/GolangStar/Go语言原理/gmp调度原理]] — schedule() 源码分析
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
---
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### Q3:Go 的 goroutine 抢占调度策略? 🟡中等
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 参考答案
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
Go 采用**抢占式调度**,由 sysmon 线程监控。不同版本的实现方式不同:
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
#### Go 1.14 之前:协作式抢占
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
- 编译器在函数调用入口插入检查代码
- G 进行函数调用时检查 `preempt` 标志
- **缺陷**:不含函数调用的超大循环不会被抢占,造成调度延迟
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
#### Go 1.14 及之后:基于信号的异步抢占
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
- sysmon 检测到运行超过 10ms 的 G
- 向 M 发送 SIGURG 信号
- gsignal goroutine 处理信号,停止正在运行的 G
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!warning] ⚠️ 高频陷阱
> Go 1.14 之前的协作式抢占无法处理"纯计算密集型且无函数调用的 goroutine",这是最常见的面试追问点。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
---
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### Q4:发生调度的时机有哪些? 🟢简单
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 参考答案
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
| 触发条件 | 说明 |
|---------|------|
| Channel 操作 | 等待读写未缓冲 channel |
| time.Sleep() | 等待睡眠时间结束 |
| Mutex 等待 | 获取互斥量失败 |
| 系统调用 | 发起 syscall 进入内核态 |
| 主动让出 | `runtime.Gosched()` |
| 抢占 | sysmon 强制抢占(Go 1.14+) |
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
---
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### 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
```
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
1. **本地队列**(LRQ):无锁 CAS 操作
2. **全局队列**(GRQ):需要加锁
3. **网络轮询器**(netpoll):IO 就绪的 G
4. **Work-stealing**:随机找其他 P 偷一半
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!tip] 💡 面试技巧
> "Go 的调度器就像一个聪明的餐厅经理:先看自己桌有没有客人(本地队列),没有就去公共排队区(全局队列),再去门口看看有没有新到的人(netpoll),最后实在没人就从隔壁桌'借'几个客人来(steal)。"
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
---
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### Q6:GMP 能不能去掉 P 层? 🟡中等
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 参考答案
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
**理论上可以,但会带来严重的性能问题。**
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
| 方案 | 问题 |
|------|------|
| 去掉 P(变成 GM) | 所有 M 争抢全局锁,高并发下锁竞争严重 |
| 保留 P | 每个 P 独立本地队列,大部分情况无需全局锁 |
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
> [!note] 📝 核心考点
> P 的价值在于实现了**无锁的本地调度**。M 绑定 P 后直接从本地队列取 G,只有队列空了才去偷取,大大减少了锁竞争。
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
---
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
### Q7:P 和 M 在什么时候被创建? 🟡中等
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
## 参考答案
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
| 组件 | 创建时机 | 说明 |
|------|---------|------|
| **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调度原理]] — 栈切换的汇编实现细节
## 关联笔记
2026-06-07 11:08:10 +08:00
2026-06-07 12:14:39 +08:00
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
- [[hzh/GolangStar/Go语言原理/gmp调度原理]]
- [[hzh/GolangStar/Go面试题库/内存管理面试题]]