Table of contents
Open Table of contents
TL;DR
mio 刻意只做事件通知,不提供 timer、buffer 管理、async/await、任务调度。直接用 mio 写服务器代码就像用 epoll_wait 的 C 代码一样繁琐。tokio 在 mio 之上叠加了完整的异步运行时。这不是 mio 的缺陷——这是 Unix 哲学的 “do one thing well”。
mio 不做什么
| 功能 | mio 提供吗 | 谁提供 |
|---|---|---|
| 事件通知(readable/writable) | 是 | — |
| Non-blocking TCP/UDP/Unix socket | 是 | — |
| 跨线程唤醒 | 是(Waker) | — |
| 定时器 | 否 | tokio::time |
| Buffered I/O | 否 | tokio::io::BufReader |
| async/await 集成 | 否 | tokio / async-std |
| 任务调度(spawn/join) | 否 | tokio runtime |
| Channel(mpsc/oneshot/broadcast) | 否 | tokio::sync |
| DNS 解析 | 否 | tokio::net::lookup_host |
| TLS/SSL | 否 | tokio-rustls / tokio-native-tls |
| HTTP | 否 | hyper / reqwest |
| 信号处理 | 否 | tokio::signal |
| 文件 I/O | 否 | tokio::fs(通过线程池) |
mio 的职责边界极其清晰:把 OS 的 I/O 事件通知以类型安全的方式暴露出来,仅此而已。
直接用 mio 的代码长什么样
一个最基本的 TCP echo server(接受连接,echo 回去),用 mio 要写大约 150 行。你需要手动:
- 创建
Poll和Events - 绑定
TcpListener并 register - 手写事件循环
loop { poll.poll(...) ... } - 手动管理连接池(
HashMap<Token, TcpStream>) - 手动分配 Token(递增计数器)
- 手动处理
WouldBlock、Interrupted错误 - 手动处理部分读/写
- 手动 drain 所有可读数据(边缘触发的要求)
- 手动 register/deregister 新连接
对比 C++ 用原始 epoll 写的代码——复杂度几乎相同。mio 只是把系统调用包成了 Rust 类型,不减少任何概念负担。
tokio 在 mio 之上加了什么
1. I/O Driver:把 mio 的 Poll 融入 runtime
tokio 的 runtime::io::Driver 内部持有:
// tokio 源码 (tokio/src/runtime/io/driver.rs)
struct Driver {
poll: mio::Poll, // mio 的 Poll
events: mio::Events, // 复用的事件集合
// ...
}
tokio 的多线程 runtime 会在 worker thread 中调用 mio::Poll::poll(),然后把收到的事件转换成 tokio 的 Ready 状态,唤醒对应的 async task。
关键桥接逻辑:
- tokio 用
mio::Waker创建一个TOKEN_WAKEUP,让其他线程能唤醒正在 poll 的 worker - 每个 tokio 的
TcpStream内部持有mio::net::TcpStream - 当用户
.await一个read()时,tokio 把mio::Interest::READABLE注册到 mio,然后 yield - mio 返回 readable 事件后,tokio 唤醒对应的 Future,用户代码继续执行
2. Timer:mio 刻意不做的东西
mio 没有定时器。tokio 自己实现了一个分层时间轮(hierarchical timing wheel),与 mio 的 poll timeout 集成:
// tokio 在调用 mio::Poll::poll() 时,计算最近的定时器到期时间
// 作为 poll 的 timeout 参数
let timeout = timer_wheel.next_deadline();
poll.poll(&mut events, timeout)?;
// poll 返回后,既检查 I/O 事件,也检查到期的定时器
3. Task Scheduler:mio 完全不涉及的领域
mio 不知道什么是 “task”。tokio 的 task scheduler 管理数千个并发任务:
tokio::spawn()创建轻量级任务- Work-stealing 调度器在多个线程间均衡负载
- 每个 task 是一个 state machine(由 async/await 编译生成)
4. Buffered I/O:mio 给你原始字节
mio 的 TcpStream::read() 直接调系统调用,返回原始字节。tokio 提供:
AsyncBufReadtrait — 带 buffer 的异步读BufReader/BufWriter— 减少系统调用次数codec(通过 tokio-util)— 把字节流解析成消息帧
对比总结
用 mio 写服务器:
loop {
poll.poll(&mut events, timeout)?;
for event in events.iter() {
match event.token() {
SERVER => {
// 手动 accept
// 手动分配 token
// 手动注册新连接
}
token => {
// 手动查找连接
// 手动处理 WouldBlock
// 手动 drain 数据
// 手动检查连接关闭
}
}
}
}
用 tokio 写同样的服务器:
let listener = TcpListener::bind("127.0.0.1:9000").await?;
loop {
let (mut socket, addr) = listener.accept().await?;
tokio::spawn(async move {
let (mut rd, mut wr) = socket.split();
io::copy(&mut rd, &mut wr).await.unwrap();
});
}
tokio 版本大约 10 行。所有 mio 层面的复杂性——事件循环、Token 分发、WouldBlock 处理、连接管理——都被 async/await 和 tokio runtime 吸收了。
mio 0.6 曾经更「完整」
值得一提的是,mio 曾经提供更多功能:
- mio 0.6 有内置 timer:
mio::timer::Timer可以设置定时事件 - mio 0.6 有内置 channel:
mio::channel::Sender/Receiver可以跨线程发送消息 - mio 0.6 有 EventLoop:
EventLoop+Handlertrait 提供了一个完整的事件循环框架
从 0.7 开始,这些全被删除了。理由:
- 职责分离:timer 和 channel 不是 I/O 事件通知,不该放在 mio 里
- tokio 做得更好:tokio 的 timer 和 channel 更完善、更高效
- 减少维护负担:mio 团队专注于把 OS 事件通知做到最好
这个演进方向与 Asio 相反——Asio 越来越厚(strand → 协程 → cancellation slot → deferred),mio 越来越薄。
什么时候该直接用 mio
极少数场景:
- 你在写自己的异步运行时:如果你在做一个 tokio 的竞品(如 glommio、monoio),mio 就是你的起点
- 嵌入式 / 资源受限环境:不想引入 tokio 的全部依赖,只要事件通知
- 学习操作系统 I/O 多路复用:mio 比直接用 libc 调 epoll 稍微安全一点,适合学习
- 与 C 库集成:某些 C 库给你一个 fd,你想把它纳入事件循环但不想依赖 tokio
对于 99% 的应用开发,用 tokio(或 async-std)。mio 是给基础设施开发者的工具。