首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >面试官问 Go 的 GMP 模型,这样回答直接进了下一轮

面试官问 Go 的 GMP 模型,这样回答直接进了下一轮

作者头像
王中阳AI编程
发布2026-04-16 17:41:00
发布2026-04-16 17:41:00
1220
举报
文章被收录于专栏:Go语言学习专栏Go语言学习专栏

今天聊一个后端面试必考的高频硬核知识点:Go 语言的 GMP 调度模型。我会用大白话把 G、M、P 是什么、调度流程、阻塞处理、work stealing 等细节拆开讲清楚。另外还附带 Channel、GC、MySQL 索引、Redis 等常考内容,帮你一次备全。

一、GMP 模型(Go 调度器核心)—— 这样回答才算过关

面试官只要问“Go 的并发原理”,99% 会接着问 GMP。你需要从概念 → 调度流程 → 阻塞场景 → 优化机制 逐层讲透。

1. 三个核心概念

组件

全称

作用

类比

G

Goroutine

代表一个任务,包含栈、指令指针、状态等信息

待执行的“函数包裹”

M

Machine

内核线程,真正执行代码的实体

干活的人

P

Processor

逻辑处理器,持有本地 G 队列,负责调度

人的“任务篮子”

关键约束:

  • G 必须绑定到 M 才能执行
  • P 的数量由 GOMAXPROCS 决定,默认等于 CPU 核数
  • M 的数量可以多于 P,阻塞时会创建新 M

2. 调度流程(正常情况)

  • 每个 P 有一个本地队列(LRQ),存待执行的 G,无锁访问,效率高
  • 还有全局队列(GRQ),存长时间等待或偷取来的 G,需要加锁

当 P 的本地队列为空时

  1. 先从全局队列取一批(最多取 len(GRQ)/GOMAXPROCS + 1 个)
  2. 如果全局队列也为空,触发 work stealing
    • 随机选择一个其他 P
    • 偷取其本地队列中一半的 G(从尾部偷)
    • 这样既能负载均衡,又减少锁竞争

3. 阻塞场景的处理(面试必问深水区)

场景一:G 执行网络 I/O(如 conn.Read
  • Go 使用 netpoller(网络轮询器,基于 epoll/kqueue)
  • G 发起非阻塞读 → 数据未就绪 → G 被标记为等待,放入 netpoller 的等待队列
  • G 与 M、P 解绑,P 立即取下一个 G 继续执行
  • 当网络数据到达,netpoller 唤醒 G,将其放回某个 P 的本地队列

结果:没有系统调用阻塞,没有线程切换,高效。

场景二:G 执行阻塞系统调用(如文件读写、time.Sleep
  • 此时无法用非阻塞模式,G 会真正阻塞在内核态
  • 流程:
    • 如果有空闲 P,绑定后继续执行
    • 如果没有,G 放入全局队列,M1 进入休眠或销毁
    1. 当前 M(设为 M1)带着 G 进入内核等待
    2. G 的状态变为 _Gsyscall
    3. P 与 M1 解绑,P 去找另一个空闲 M(M2)
    4. 如果没有空闲 M,runtime 会新建一个 M
    5. P 绑定 M2 继续调度其他 G
    6. 系统调用完成后,G 被唤醒,尝试重新获取一个 P:

这种机制叫 hand off:P 不等待慢系统调用,立即转移给其他 M,保证 CPU 利用率。

场景三:G 执行同步操作(如 mutex.Lock 竞争失败)
  • 这是用户态阻塞,不涉及内核
  • G 被挂到锁的等待队列,状态变为 _Gwaiting
  • G 与 M、P 解绑,P 取下一个 G 执行
  • 当锁被释放,等待队列中的 G 被唤醒,重新进入 P 的本地队列

4. 自旋线程与空闲 P

  • 如果 P 的本地队列为空,且全局队列和 work stealing 都没有任务,P 会进入空闲状态
  • 为了避免频繁创建销毁 M,Go 会让部分 M 进入自旋
    • 自旋线程会反复检查是否有新 G 到达
    • 最多有 GOMAXPROCS 个自旋线程
    • 自旋超过一段时间仍无任务,线程休眠

5. 永久等待(Goroutine 泄漏)

原因

例子

后果

无缓冲 channel 读写未配对

两个 G 都在等对方发

两个 G 永远挂起

锁未释放

mu.Lock() 后 return 没有 Unlock

等待该锁的所有 G 永久阻塞

WaitGroup 计数错误

Add(1) 但 Done() 少调用一次

调用 Wait() 的 G 永远等

网络 I/O 无超时

conn.Read 对方不响应

G 永久阻塞在 netpoller

死循环

for {} 且没有让出 CPU

虽然 Go 1.14 后支持抢占,但仍有极端情况

解决套路

  • 使用 defer 保证解锁 / Done
  • 给所有阻塞操作加超时:context.WithTimeouttime.After
  • 监听退出 channel,在循环里 select

6. 面试回答话术(可直接背)

“GMP 中 G 是 goroutine,M 是内核线程,P 是逻辑处理器。P 的数量默认等于 CPU 核数,每个 P 有一个本地 G 队列。调度时 P 优先从本地队列取 G 绑定 M 执行;本地队列空了就从全局队列或偷取其他 P 的任务。遇到网络 I/O 阻塞时,G 被 netpoller 挂起,P 立即去执行其他 G;遇到系统调用阻塞时,当前 M 带着 G 进内核,P 会解绑并 hand off 给另一个 M,保证 CPU 不空转。这样设计使得 Go 可以轻松支持数十万并发。”

二、Channel 在业务中怎么用?

常规用法

  • 协程间数据传递
  • 任务队列 / 工作池:带缓冲 channel + 固定 worker
  • 替代 WaitGroup:无缓冲 channel 阻塞等待
  • select 多路复用 + 超时 / 退出监听
  • 发送退出信号:close(ch) 广播

业务案例

案例一:批量数据接收 车辆上报数据 → 写入 channel → 4 个 worker 解析入库 → 扛住高峰流量

案例二:服务安全退出 监听 SIGTERM → 退出 channel 通知所有 G 停止新任务 → 等待当前任务完成 → 释放资源

三、为什么 Go 选用 goroutine 而不是进程或线程?

对比维度

进程

线程

goroutine

资源占用

最重

中等(栈 ~1MB)

极轻(栈 ~2KB,可扩容)

创建/销毁

内核态,慢

内核态,慢

用户态,快

调度

内核

内核

Go runtime(协作+抢占)

并发能力

几百

几千

几十万

结论:goroutine 足够轻,一台服务器轻松几十万个。

四、哪些操作会陷入内核态?Go 怎么应对?

会陷入内核态的操作

  • 线程创建/销毁/阻塞/唤醒
  • 系统调用:文件 I/O、time.Sleep
  • 同步原语竞争时的阻塞

Go 的应对

  • 网络 I/O 用 netpoller 转为非阻塞,不陷入内核
  • 阻塞系统调用时,P 解绑 M 并 hand off 给其他 M,不浪费 CPU

五、栈空间里存什么?有什么用?

栈中存储

  • 函数栈帧(局部变量、参数、返回值地址)
  • 函数调用上下文(返回地址、BP)
  • 临时计算结果
  • 栈大小和扩容标记

栈的开辟流程

  1. 程序启动初始化栈
  2. 函数调用检查空间 → 不够则扩容(连续栈,复制到 2 倍)
  3. 执行函数
  4. 返回释放栈帧

作用

  • 支撑函数调用/返回
  • 自动回收局部变量(无需 GC)
  • 每 G 独立栈 → 并发安全

六、垃圾回收(GC)

GC 作用

自动回收堆上不再被引用的对象。栈上的变量随函数返回自动释放。

逃逸到堆的对象

  • 返回局部变量的指针
  • 大对象(栈放不下)
  • 动态大小对象(make([]int, n),n 是变量)
  • 大对象赋值给接口
  • 字符串转切片

GC 原理

并发三色标记清除 + 混合写屏障。标记可达对象为黑色,回收白色对象,与用户代码并发。

优化建议

调整 GOGC,使用 sync.Pool 复用对象。

七、MySQL InnoDB 为什么用 B+ 树?

对比其他结构

对比项

二叉搜索树

B 树

哈希表

B+ 树

树高

-

磁盘 I/O

少(仅等值)

范围查询

一般

需回溯

不支持

高效(叶子链表)

B+ 树再平衡

插入:叶子节点满则分裂,中间 key 上移,递归至根 → 树高可能增加 删除:节点 key 不足则先借后合并,递归至根 → 树高可能减少

八、Redis 数据结构与底层实现

类型

底层

场景

命令

String

SDS

缓存、锁、计数器

SET, GET, INCR

Hash

压缩列表/哈希表

对象属性

HSET, HGET

List

双向链表/快表

消息队列

LPUSH, RPOP

Set

整数集合/哈希表

标签

SADD, SISMEMBER

ZSet

跳表+哈希表

排行榜

ZADD, ZREVRANGE

Bitmap

SDS 按位

签到

SETBIT, GETBIT

SDS 优势:O(1) 长度,二进制安全。

最后

GMP 是 Go 面试的分水岭。能讲清楚调度流程、阻塞处理、work stealing、hand off 机制,面试官就会认可你的底层功底。其他知识点也建议结合项目经验说,不要背概念。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 王中阳 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、GMP 模型(Go 调度器核心)—— 这样回答才算过关
    • 1. 三个核心概念
    • 2. 调度流程(正常情况)
    • 3. 阻塞场景的处理(面试必问深水区)
      • 场景一:G 执行网络 I/O(如 conn.Read)
      • 场景二:G 执行阻塞系统调用(如文件读写、time.Sleep)
      • 场景三:G 执行同步操作(如 mutex.Lock 竞争失败)
    • 4. 自旋线程与空闲 P
    • 5. 永久等待(Goroutine 泄漏)
    • 6. 面试回答话术(可直接背)
  • 二、Channel 在业务中怎么用?
    • 常规用法
    • 业务案例
  • 三、为什么 Go 选用 goroutine 而不是进程或线程?
  • 四、哪些操作会陷入内核态?Go 怎么应对?
    • 会陷入内核态的操作
    • Go 的应对
  • 五、栈空间里存什么?有什么用?
    • 栈中存储
    • 栈的开辟流程
    • 作用
  • 六、垃圾回收(GC)
    • GC 作用
    • 逃逸到堆的对象
    • GC 原理
    • 优化建议
  • 七、MySQL InnoDB 为什么用 B+ 树?
    • 对比其他结构
    • B+ 树再平衡
  • 八、Redis 数据结构与底层实现
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档