GMP 中 G 阻塞了会发生什么
在Go的GMP模型中,当某个Goroutine(G)发生阻塞时,调度器会根据阻塞类型采取不同的处理策略,确保程序的并发性能不受影响。
# 一、阻塞类型分类
Go中的阻塞操作主要分为两类:
- 系统调用导致M阻塞(如文件I/O、同步操作等)。
- 用户态导致G阻塞(如channel操作、定时器、网络I/O通过异步轮询等)。
# 二、处理系统调用阻塞
当G执行同步阻塞的系统调用(如文件读写)时,会导致底层线程(M)被操作系统挂起。此时调度器会进行以下操作:
# 1. 解绑M与P:
当前M会与关联的P(Processor)分离,P被释放以便其他M使用。
# 2. 创建或唤醒新的M:
调度器会寻找空闲的M(或创建新的M)来接管被释放的P,继续执行P本地队列中的其他G。
# 3. 系统调用返回后的处理:
- 阻塞的G完成系统调用后,会被标记为可运行状态,重新加入某个P的队列(通常是全局队列)。
- 原M尝试与空闲的P绑定以继续执行该G。若无可用P,则G进入全局队列,M进入休眠或被销毁。
# 三、处理用户态阻塞
当G因用户态操作阻塞(如channel通信、select
、time.Sleep
或异步网络I/O)时,调度器不会阻塞M,而是直接切换G:
# 1. 挂起当前G:
G会被移出P的运行队列,并加入对应事件的等待队列(如channel的发送/接收队列、定时器堆等)。
# 2. M继续执行其他G:
M立即从P的本地队列中获取下一个G执行,无需与P解绑。
# 3. 事件触发后的恢复:
当阻塞条件满足(如channel数据就绪、定时器到期、网络I/O完成),G会被重新激活,并加入某个P的队列等待调度。
# 四、异步网络I/O的特殊优化
对于网络I/O,Go通过**非阻塞I/O + 多路复用(如epoll/kqueue)**实现高效调度:
- G发起网络请求后,文件描述符会被注册到
netpoll
监听队列。 - M继续执行其他G,而非阻塞等待。
- 当
netpoll
检测到I/O事件就绪时,关联的G会被唤醒并重新加入P的队列。
# 五、关键设计优势
- P的复用:解绑M与P后,确保P始终有M执行任务,充分利用CPU。
- M的弹性伸缩:通过休眠或新建M动态调整线程数,避免资源浪费。
- 减少线程切换:用户态阻塞无需线程切换,降低开销。
# 六、总结
- 系统调用阻塞:分离M和P,P另寻M执行其他G,阻塞结束后G重新入队。
- 用户态阻塞:直接切换G,M继续运行,事件触发后恢复G。
- 网络I/O:通过异步轮询避免阻塞M,提升并发性能。
这种机制确保了Go程序在高并发场景下仍能高效利用资源,避免因单个G的阻塞导致整体性能下降。
编辑 (opens new window)
上次更新: 2025/04/16, 15:01:25