事件循环:epoll 语义模拟(LT / ET)
难度:⭐⭐⭐ 中等偏难
考点
- IO 多路复用核心流程:注册 → 等待就绪 → 分发
- 水平触发 LT vs 边缘触发 ET 的区别
- 就绪事件集合 + 回调分发的工程写法
题目描述
用内存结构模拟 epoll 事件循环(真实的 syscall.EpollCreate1/EpollCtl/EpollWait 版见文档 2.7,仅 Linux 可运行;本练习跨平台可直接 go test)。
实现 Loop:
Add(fd, handler, edgeTriggered):注册 fd 与其事件回调(edgeTriggered=true模拟EPOLLET)Remove(fd):注销Notify(fd):模拟"fd 就绪"(相当于epoll_wait返回该 fd)Consume(fd):手动消费掉该 fd 的就绪状态Process():把所有就绪事件分发给对应 handler,然后按触发模式处理:- LT(默认):就绪状态保留——未
Consume前每次Process都会再次回调(对应"数据没读完就绪一直存在") - ET:分发一次后自动清除就绪状态(对应"边缘只触发一次,必须循环读到 EAGAIN")
- LT(默认):就绪状态保留——未
函数签名
go
type Loop struct{ /* 自行设计 */ }
func NewLoop() *Loop
func (l *Loop) Add(fd int, handler func(fd int), edgeTriggered bool)
func (l *Loop) Remove(fd int)
func (l *Loop) Notify(fd int)
func (l *Loop) Consume(fd int)
func (l *Loop) Process()提示
- 内部维护两个 map:
handlers map[int]func(int)与ready map[int]bool,再记一个et map[int]bool Notify只对已注册的 fd 生效Process遍历就绪集合分发;ET 模式下分发后delete(ready, fd),LT 模式保留- 思考:真实 epoll 中为什么 ET 要求"循环读到 EAGAIN"?(边缘只触发一次,读不完数据就丢了)
验收
- [ ] LT 下未 Consume 会重复触发,Consume 后停止
- [ ] ET 下无论是否 Consume 只触发一次
- [ ] Remove 后的 fd 即使 Notify 也不会被分发
参考答案(Go)
点击展开参考答案
go
//go:build ignore
package answer
// Loop 参考答案:事件循环(LT / ET 触发语义)
type Loop struct {
handlers map[int]func(fd int)
ready map[int]bool
et map[int]bool
}
func NewLoop() *Loop {
return &Loop{
handlers: make(map[int]func(fd int)),
ready: make(map[int]bool),
et: make(map[int]bool),
}
}
func (l *Loop) Add(fd int, handler func(fd int), edgeTriggered bool) {
l.handlers[fd] = handler
l.et[fd] = edgeTriggered
}
func (l *Loop) Remove(fd int) {
delete(l.handlers, fd)
delete(l.ready, fd)
delete(l.et, fd)
}
func (l *Loop) Notify(fd int) {
if _, ok := l.handlers[fd]; ok {
l.ready[fd] = true
}
}
func (l *Loop) Consume(fd int) {
delete(l.ready, fd)
}
func (l *Loop) Process() {
for fd := range l.ready {
h, ok := l.handlers[fd]
if !ok {
continue
}
h(fd)
if l.et[fd] {
delete(l.ready, fd)
}
}
}