Golang 网络轮询器 netpoll:把阻塞 IO 变成事件通知

网络轮询器 netpoll:把阻塞 IO 变成事件通知

Go 的 net/http 能轻松支撑数十万并发连接,秘密之一就是 netpoll。它让 goroutine 在等待网络 IO 时,不会阻塞底层的 OS 线程,而是把 socket 注册到操作系统提供的多路复用机制上,事件就绪后再唤醒对应的 G。

一、为什么需要 netpoll

假设一个 goroutine 执行 conn.Read(),如果 OS 线程真的阻塞在 read 系统调用上,那其他 goroutine 就没法用这个 M 了。Go 的做法是:

  1. G 需要等待网络事件时,调用 gopark 挂起自己。
  2. socket 被注册到 epoll/kqueue。
  3. 某个 M 在调度循环中调用 netpoll(),获取就绪的 socket 列表。
  4. 把对应的 G 标记为可运行,放回 LRQ。

这样,一个 M 就能同时照看成千上万个等待中的网络连接。

二、跨平台实现

Go runtime 在不同操作系统上使用不同的多路复用机制:

平台 机制
Linux epoll
macOS / BSD kqueue
Windows IOCP

接口被封装成统一的形式:netpollinitnetpollopennetpollclosenetpoll

三、pollDesc 与文件描述符的绑定

每个网络 socket 都会关联一个 pollDesc 结构,核心字段包括:

text 复制代码
type pollDesc struct {
	link *pollDesc          // 链表指针
	fd   uintptr            // 文件描述符
	rg   uintptr             // 等待读事件的 G 或 gList
	wg   uintptr             // 等待写事件的 G 或 gList
	user uint32              // 用户事件掩码
}

当 G 调用 netpollblock(pd, mode) 时,会把当前 G 挂到 rg(读)或 wg(写)上,然后 gopark 让出 CPU。事件就绪后,netpollready 唤醒对应的 G。

四、netpoll 什么时候被调用

调度循环会在合适的时机调用 netpoll()

  1. schedule() 找不到可运行的 G 时。
  2. sysmon 系统监控线程检测到 netpoll 结果需要处理时。
  3. 显式的网络相关阻塞/唤醒路径中。

netpoll() 有一个 timeout 参数。如果为 0,表示非阻塞轮询一次;如果大于 0,可以让出 CPU 等待事件。

五、从用户代码到 netpoll 的路径

net.TCPConn.Read 为例,简化路径是:

text 复制代码
conn.Read(buf)
  └── poll.FD.Read
        └── fd.pd.waitRead()
              └── netpollblock(&fd.pd, 'r', true)
                    └── gopark(...) // G 被挂起,M 继续调度其他 G

当 socket 有数据可读时,epoll/kqueue 通知 runtime,netpoll 遍历就绪事件,调用 netpollready 把 G 状态改回 _Grunnable

六、代码实践:模拟网络 IO 事件等待

下面用 net.Pipe() 创建一对内存 pipe,演示 goroutine 等待另一端写入。虽然 pipe 不完全等于 socket,但它同样会触发 netpoll 路径。

go 复制代码
package main

import (
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)

func main() {
	c1, c2 := net.Pipe()
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		buf := make([]byte, 128)
		fmt.Println("reader: waiting for data at", time.Now().Format("15:04:05.000"))
		n, err := c1.Read(buf)
		if err != nil && err != io.EOF {
			fmt.Println("reader error:", err)
			return
		}
		fmt.Printf("reader: got %q at %s\n", string(buf[:n]), time.Now().Format("15:04:05.000"))
	}()

	go func() {
		defer wg.Done()
		time.Sleep(300 * time.Millisecond)
		fmt.Println("writer: sending at", time.Now().Format("15:04:05.000"))
		_, err := c2.Write([]byte("hello netpoll"))
		if err != nil {
			fmt.Println("writer error:", err)
		}
		c2.Close()
	}()

	wg.Wait()
}

输出显示 reader 在 Read 处等待 300ms,直到 writer 写入数据。等待期间,reader 所在的 G 被 park,M 可以去执行其他 goroutine。

七、用 TCP Listener 观察 Accept 等待

go 复制代码
package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	defer ln.Close()

	addr := ln.Addr().String()
	fmt.Println("listen on", addr)

	// 延迟 200ms 再连接
	go func() {
		time.Sleep(200 * time.Millisecond)
		conn, err := net.Dial("tcp", addr)
		if err != nil {
			fmt.Println("dial error:", err)
			return
		}
		defer conn.Close()
		fmt.Println("client: connected at", time.Now().Format("15:04:05.000"))
	}()

	fmt.Println("server: waiting for accept at", time.Now().Format("15:04:05.000"))
	conn, err := ln.Accept()
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	fmt.Println("server: accepted at", time.Now().Format("15:04:05.000"))
}

Accept 也是一个会走 netpoll 的阻塞调用。在等待连接期间,G 被 park,不会占用 M。

八、大量连接下的 M 数量

正是因为 netpoll,Go 程序即使有几十万个等待 IO 的 goroutine,M 的数量也可以保持在一个合理范围。没有被 IO 阻塞的 M 可以去执行其他 G,而 epoll/kqueue 负责在事件就绪时通知 runtime。

九、逐条解读

  1. net.Pipe() 创建一对全双工连接,底层是匿名 socket。
  2. c1.Read() 阻塞时,当前 G 调用 gopark 挂起,socket 被注册到 netpoll。
  3. 300ms 后 c2.Write() 写入数据,触发 epoll/kqueue 就绪事件。
  4. netpoll 在调度循环中被调用,拿到就绪的 G,放回 LRQ。
  5. ln.Accept() 同样走 netpoll 路径,等待连接事件。
  6. 这种设计让少量 M 能支撑大量并发网络连接。

十、小结

概念 一句话总结
netpoll Go runtime 的网络事件轮询器,把阻塞 IO 变成异步事件
epoll/kqueue/IOCP 操作系统提供的多路复用机制
pollDesc socket 与等待 G 之间的关联结构
gopark/goready 挂起和唤醒 goroutine 的核心原语
netpollblock 把 G 注册到 pollDesc 并 park
netpollready 事件就绪后唤醒 G
相关推荐
newerp1 小时前
Golang 函数调用的瞬间:栈帧、拷贝栈与逃逸分析
后端·程序员·go
会编程的吕洞宾1 小时前
LangChain4j RAG 分块策略实战:检索质量提升 80% 的 Chunking 调优全解
java·人工智能·后端
万物智能1 小时前
HDMI 输出—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
后端·架构
小阿鑫1 小时前
AI 越来越强,为什么打工人反而越来越累、越来越内耗了?
人工智能·ai·程序员·职业发展·agi·工作效率
Cache技术分享1 小时前
516. Java 方法句柄 - 转换(进阶篇·下)
前端·后端
妙码生花1 小时前
全网 8k star 的 BuildAdmin 正式发布 Golang 版本,这次我们在CRUD赛道杀死了比赛。
前端·后端·ai编程
Zane19941 小时前
复制算法明明要浪费一半内存,为什么新生代还偏偏用它
java·后端
写代码像蔡徐抻1 小时前
三年了,AI为何还没有抢走程序员饭碗?
前端·后端·面试
魔兽大山哥1 小时前
【NL2SQL 实战 08】主链路:从提问到出数,图里跑了哪些节点
后端