mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1236 字
3 分钟
事件循环(Event Loop)
2026-06-13

一、为什么需要事件循环#

假设你要写一个聊天服务器,同时服务 10000 个连接。传统做法是每个连接分配一个线程,但每个线程需要 1-8 MB 栈空间,10000 个连接就是 10-80 GB 内存——还没开始处理业务,内存就先爆了。更糟糕的是,大部分连接大部分时间都在等 I/O(等用户发消息),线程白白占着内存和 CPU 时间片。

线程模型的另一个问题是上下文切换。操作系统在成千上万个线程间来回切换,每次切换都要保存和恢复寄存器、栈指针、缓存失效——这些开销在连接数上去后变得不可忽视。

事件循环换了一个思路:与其让 10000 个线程各等各的,不如让一个线程统一等。用操作系统的 I/O 多路复用(epollkqueueIOCP)同时监听所有连接,哪个有数据就处理哪个。这就是 Node.js 在单线程上处理万级并发连接的原理。

二、现实类比#

一个人处理整个办公室电话的前台。她不能同时和两个人说话,但她把每个电话放在等待中,处理快速事务,然后轮流回拨。如果某件事耗时,她记下来继续处理下一件——绝不阻塞在任何一个来电上。

三、核心思想#

事件循环的核心是一个三步循环:注册兴趣 → 轮询就绪 → 分发处理。

flowchart TD A[注册 I/O 兴趣] --> B[轮询就绪事件<br/>epoll/kqueue 阻塞] B --> C[分发给回调处理] C --> A

以 libuv(Node.js 底层)为例,每一轮循环包含多个阶段:

flowchart LR A[Timers] --> B[Pending Callbacks] B --> C[Poll I/O] C --> D[Check / setImmediate] D --> E[Close Callbacks] E --> A
属性
并发模型单线程,非阻塞 I/O
连接数每线程数千(受文件描述符限制,非线程限制)
延迟I/O 密集型低延迟;一个慢回调会阻塞所有
内存O(连接数) 用于状态,非 O(连接数 × 栈大小)

四、变体与对比#

模式关系区别
协作调度事件循环依赖协作调度协作调度是「任务主动让出」的调度策略;事件循环是「单线程轮询分发」的架构模式
Actor 模型每个 Actor 本质上是信箱上的事件循环Actor 强调消息传递和状态隔离;事件循环强调 I/O 多路复用
观察者模式事件循环将事件分发给注册的回调观察者是设计模式层面的概念;事件循环是运行时架构
Reactor 模式事件循环的另一种叫法Reactor 更强调「事件到来时回调」的反应式语义

五、多语言实现#

Go:基于 channel 的简易事件循环#

Go 有 goroutine,通常不需要手写事件循环。但理解其原理很有价值:

type EventLoop struct {
handlers map[int]func()
}
func NewEventLoop() *EventLoop {
return &EventLoop{handlers: make(map[int]func())}
}
func (el *EventLoop) AddHandler(fd int, handler func()) {
el.handlers[fd] = handler
}
func (el *EventLoop) Tick() int {
count := len(el.handlers)
for _, handler := range el.handlers {
handler()
}
return count
}
func (el *EventLoop) Run(maxTicks int) int {
ticksRun := 0
for i := 0; i < maxTicks; i++ {
if len(el.handlers) == 0 { break }
el.Tick()
ticksRun++
}
return ticksRun
}

TypeScript:带阶段的事件循环#

type Handler = () => void;
class EventLoop {
private handlers = new Map<number, Handler>();
private timerQueue: { deadline: number; handler: Handler }[] = [];
addHandler(fd: number, callback: Handler): void {
this.handlers.set(fd, callback);
}
// 注册定时器回调
addTimer(delayMs: number, handler: Handler): void {
this.timerQueue.push({
deadline: Date.now() + delayMs,
handler,
});
// 按截止时间排序
this.timerQueue.sort((a, b) => a.deadline - b.deadline);
}
tick(): number {
const now = Date.now();
// 阶段 1:执行到期的定时器
while (this.timerQueue.length > 0 && this.timerQueue[0].deadline <= now) {
this.timerQueue.shift()!.handler();
}
// 阶段 2:执行 I/O 回调
for (const [, handler] of this.handlers) {
handler();
}
return this.handlers.size;
}
}

六、生产验证#

  • libuvcore.c#L427-L492uv_run 是 Node.js 使用的主事件循环函数,在单个 while 循环中处理定时器、I/O 轮询、check 句柄和关闭句柄,支持 DEFAULT/ONCE/NOWAIT 三种运行模式
  • Redisae.c#L360-L468aeProcessEvents 是 Redis 事件循环的核心,计算最近定时器超时后调用 aeApiPoll,然后分发文件事件和定时器事件,单线程实现 10 万+ ops/sec
  • Nginx — 每个 worker 进程运行独立的 epoll/kqueue 事件循环,多进程架构实现多核利用和进程隔离

七、小结#

何时使用:

  • 高连接服务器——Web 服务器、聊天服务器、API 网关,数千连接大多空闲
  • I/O 密集型工作——网络代理、负载均衡器、数据库连接池
  • 实时通信——WebSocket 服务器、游戏服务器、通知系统
  • 嵌入式/资源受限——无法承受每连接一线程的内存开销

何时不用:

  • CPU 密集型工作——单线程事件循环会在计算上阻塞,应使用线程池或工作进程
  • 简单请求-响应——并发连接少于 100 且请求简单时,每请求一线程更简单
  • 严格排序要求——事件必须按精确到达顺序处理时,顺序队列更清晰
  • 需要多核——单线程只能用一个核,需要多进程(如 Nginx)或线程池补充

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

事件循环(Event Loop)
https://blog.souloss.com/posts/programming/concurrency/concurrency-event-loop/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时