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

7.5 KiB
Raw Blame History

tags, create time
tags create time
go
golang
interview
gmp-questions
2026-06-07 14:30

GMP 面试题 ⚙️

概述

本文件涵盖 Go GMP 调度模型的 10 道高频面试题,涉及 G/M/P 含义、调度策略、抢占机制、工作窃取等核心概念。GMP 是 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] 🔗 延伸阅读


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
  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] 🔗 延伸阅读


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] 🔗 延伸阅读

关联笔记