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

224 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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面试题库/内存管理面试题]]