← 返回首页

线程在M上被网络阻塞,会发生什么

在 Go 语言的 GMP 调度模型中,当 Goroutine(G)在物理线程(M)上执行网络 I/O 操作(如等待客户端发送数据)时,物理线程 M 本身并不会被阻塞。Go 运行时通过底层的网络轮询器(Netpoller)接管了这一过程,从而实现了高效的非阻塞并发。

具体而言,整个过程分为以下四个阶段:

1. 移交托管站(状态变更与抽离)

当 G 调用网络读取方法(如 conn.Read())且网络数据尚未到达时,Go 运行时绝不会阻塞当前的物理线程 M。相反,它会将 G 的状态从运行中(Running)改为休眠等待(Waiting),并将 G 从当前 P(逻辑处理器)的本地队列中抽离出来,直接注册到 Netpoller 的 I/O 多路复用监听列表(如 Linux 的 epoll)中。

2. M 与 P 光速解脱(零延迟切换)

由于阻塞的 G 已经被 Netpoller 接管,物理线程 M 瞬间腾出了双手。它会立刻从绑定的 P 的本地队列中取出下一个就绪的 G,继续疯狂执行。整个切换过程完全在用户态完成,物理线程 M 没有任何停顿,CPU 保持 100% 的利用率。

3. 网卡数据到达,唤醒 Netpoller

当客户端的数据通过网络到达网卡时,操作系统内核会触发中断。此时,一直在暗中观察的 Netpoller 检测到对应的 Socket 描述符(fd)已经就绪,会立刻被唤醒。Netpoller 顺着就绪的 fd 找到之前被托管的 G,将其状态重新改回可运行(Runnable),并将其捞出,放回某个 P 的本地队列(或全局队列)中。

4. G 被重新轮询执行

当某个物理线程 M 在下一次领任务时,就会领到这个“满血复活”的 G 并继续执行。对于 G 而言,它以为自己刚刚经历了一次正常的阻塞等待,但实际上,底层的物理线程 M 早就趁这段时间干完了好几个其他协程的工作。

总结:在网络 I/O 场景下,Go 的 Netpoller 机制将原本会导致线程阻塞的操作转化为协程级别的等待。这不仅避免了物理线程 M 被卡死,也避免了因每个网络连接都新建线程而导致的系统资源爆炸,是 Go 语言能够轻松支撑海量高并发网络请求的核心秘密。