Go 并发编程——网络轮询器
基于操作系统提供的 I/O 多路复用机制,如 Linux 的 epoll,将异步、非阻塞的网络 I/O 操作,封装成开发者熟悉的同步阻塞模型。
核心思想是:当 goroutine 执行网络I/O时,若操作未就绪,该 goroutine 会被挂起;一旦就绪,网络轮询器就会唤醒让其执行。增强了程序的并发处理能力。
1 设计原理
I/O多路复用
- select 可同时监听最多 1024 个文件描述符的可读或可写状态
- poll 和 select 类似,使用链表存储文件描述符,摆脱了 1024 的数量限制
多路复用函数会阻塞的监听一组文件描述符,当文件描述符的状态转变为可读或可写时,select 会返回就绪事件的个数,程序可以在输入的文件描述符中查找就绪的,然后执行对应操作。
select 的限制
- 监听能力有限
- 内存拷贝开销大,需要维护一个较大的数据结构存储文件描述符,该结构需要拷贝到内核中。
- 时间复杂度 O(n),返回就绪事件个数后,需要遍历所有的文件描述符。
不同操作系统有自己的 I/O 多路复用函数,例如:epoll、kqueue,Go 都实现了对应的网络轮询器。如果目标平台是 Linux,就会根据文件中的 // +build linux 编译指令选择 netpoll_epoll.go。
2 数据结构
pollDesc 是 Go 运行时用于管理一个文件描述符及其相关 IO 事件、等待的 goroutine 和 超时信息的内部结构体。
- 当一个 goroutine 等待读事件时,其指针会被放入 rg 字段。
- 当事件就绪时,pollDesc 的状态会更新,并唤醒 rg 或 wg 中等待的 goroutine。
3 多路复用
网络轮询器实际上是对 I/O 多路复用技术的封装。
实现原理:
- 网络轮询器的初始化
- 向网络轮询器加入待监控的任务
- 如何从网络轮询器获取触发的事件
初始化
netpollinit(),会调用 epoll_create 系统调用,创建一个 epoll 池
轮询事件
M 去执行其他的 G
事件循环
netpoll 函数
调度器获得这个列表后,会将其中处于 waiting 态的 gorotuine 置为 runnable,放入全局或本地队列。
4 小结
Go 语言的网络轮询器 (netpoller) 是一个精妙的设计,它通过以下几个层面,在系统底层和开发者接口之间构建了一座桥梁:
5 扩展阅读:在内核视角下
5.1 核心数据结构
- event poll:epoll_create 时生成的对象,包含两个重要成员
- 红黑树:用于存储所有被监控的fd,插入、删除、修改均为 O(logN)
- 就绪列表 Ready List:用于存储所有事件已经就绪的 fd 列表
- epitem(epoll item):当 epoll_ctl 添加 fd 时,会创建一个 epitem 节点添加到红黑树上,记录 fd 的状态、关注的事件类型
- 回调机制(回调函数):每个 epitem 都会与一个回调函数绑定,当 fd 对应的设备数据就绪时,内核会主动调用这个回调函数。
5.2 为什么 epoll 比 select/poll 更优越?(核心优势对比)
优势一:监听数量无上限
- select:受限于 FD_SETSIZE(通常为 1024),修改需要重新编译内核。
- poll:使用链表管理,虽无硬编码上限,但仍需遍历。
- epoll:上限为系统最大文件句柄数(如 /proc/sys/fs/file-max),理论上支持百万级并发。
优势二:高效的事件获取(时间复杂度 O(1) vs O(n))
这是性能分水岭:
- select/poll:每次调用时,需要将用户态的 FD 集合全部拷贝到内核态,且内核需要遍历整个集合(O(n)),检查每个 FD 的状态。如果连接数(n)为 100 万,而只有 1 个活跃连接,依然要遍历 100 万次。
- epoll:epoll_wait 直接返回活跃连接列表。它只处理活跃的 FD,效率与活跃连接数(m)成正比,即 O(m)。在海量空闲连接下,性能优势呈指数级放大。
优势三:零拷贝与内存映射(减少数据拷贝)
- select/poll:每次 select 调用都会将整个 FD 集合从用户态拷贝到内核态,返回时再拷贝回去,频繁的上下文切换和拷贝开销极大。
- epoll:epoll_ctl 只在添加/删除时拷贝一次 FD。同时,epoll 使用 mmap(内存映射)机制,在内核和用户空间共享一块内存,返回就绪事件时无需拷贝,直接映射读取。
优势四:边缘触发(ET)模式
- select/poll 仅支持水平触发(LT),即只要数据没读完,每次调用都会通知你,容易导致频繁的系统调用。
- epoll 引入了边缘触发(ET) 模式。它只告诉应用程序 “从无到有” 的那一瞬间。一旦通知,应用层必须一次将数据全部读取完毕,否则下次不再通知。这极大地减少了 epoll_wait 的调用次数,配合非阻塞 I/O 可以达到极高的吞吐量(Go 的 netpoller 默认采用 ET 模式)。




