跳转到正文
zeno's blog
返回

mio(四):为什么不直接用 mio

专题: mio

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/Otokio::io::BufReader
async/await 集成tokio / async-std
任务调度(spawn/join)tokio runtime
Channel(mpsc/oneshot/broadcast)tokio::sync
DNS 解析tokio::net::lookup_host
TLS/SSLtokio-rustls / tokio-native-tls
HTTPhyper / reqwest
信号处理tokio::signal
文件 I/Otokio::fs(通过线程池)

mio 的职责边界极其清晰:把 OS 的 I/O 事件通知以类型安全的方式暴露出来,仅此而已。

直接用 mio 的代码长什么样

一个最基本的 TCP echo server(接受连接,echo 回去),用 mio 要写大约 150 行。你需要手动:

  1. 创建 PollEvents
  2. 绑定 TcpListener 并 register
  3. 手写事件循环 loop { poll.poll(...) ... }
  4. 手动管理连接池(HashMap<Token, TcpStream>
  5. 手动分配 Token(递增计数器)
  6. 手动处理 WouldBlockInterrupted 错误
  7. 手动处理部分读/写
  8. 手动 drain 所有可读数据(边缘触发的要求)
  9. 手动 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。

关键桥接逻辑:

  1. tokio 用 mio::Waker 创建一个 TOKEN_WAKEUP,让其他线程能唤醒正在 poll 的 worker
  2. 每个 tokio 的 TcpStream 内部持有 mio::net::TcpStream
  3. 当用户 .await 一个 read() 时,tokio 把 mio::Interest::READABLE 注册到 mio,然后 yield
  4. 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 管理数千个并发任务:

4. Buffered I/O:mio 给你原始字节

mio 的 TcpStream::read() 直接调系统调用,返回原始字节。tokio 提供:

对比总结

用 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 曾经提供更多功能:

从 0.7 开始,这些全被删除了。理由:

  1. 职责分离:timer 和 channel 不是 I/O 事件通知,不该放在 mio 里
  2. tokio 做得更好:tokio 的 timer 和 channel 更完善、更高效
  3. 减少维护负担:mio 团队专注于把 OS 事件通知做到最好

这个演进方向与 Asio 相反——Asio 越来越厚(strand → 协程 → cancellation slot → deferred),mio 越来越薄。

什么时候该直接用 mio

极少数场景:

  1. 你在写自己的异步运行时:如果你在做一个 tokio 的竞品(如 glommio、monoio),mio 就是你的起点
  2. 嵌入式 / 资源受限环境:不想引入 tokio 的全部依赖,只要事件通知
  3. 学习操作系统 I/O 多路复用:mio 比直接用 libc 调 epoll 稍微安全一点,适合学习
  4. 与 C 库集成:某些 C 库给你一个 fd,你想把它纳入事件循环但不想依赖 tokio

对于 99% 的应用开发,用 tokio(或 async-std)。mio 是给基础设施开发者的工具。


分享这篇文章:

上一篇
整洁架构(三):方法设计-每层用自己的语言命名和传参
下一篇
整洁架构(二):核心原则-依赖方向永远从外向内