mio
Goal of This Episode
Meet mio — the tool that makes “one Thread watching a big pile of I/O sources” possible, and the foundation of next episode’s reactor.
Main Text
The reactor’s Role in the runtime
Let’s step back and look at the full picture of the runtime we’re building. A runtime really has two roles, each with its own duty:
- executor: takes
Tasks off the ready queue andpolls them — “runningTasks.” It knows nothing about the outside world — not whether a network packet has arrived, nor whether a file read is done. - reactor: watches all the I/O sources, and
wakes the correspondingTaskwhen one becomes ready — “waiting for events.” It does notpollFutures and is not aTask; it only watches external event sources.
In recent episodes our “waiting” relied on one Thread per Delay — far too wasteful. The reactor’s mission is to watch many I/O sources with one Thread. The way to do that is this episode’s star: mio.
mio’s Two Protagonists
mio is the low-level crate in the Rust ecosystem for cross-platform I/O event notification (Tokio uses it internally too). Add the dependency before use:
[dependencies]
mio = { version = "1", features = ["os-poll", "net"] }
Note one thing first: mio itself is not async. It doesn’t know about async fn, won’t build Futures for you, and won’t .await on your behalf. What it provides is lower-level: for instance, setting a socket to non-blocking, registering it with a Poll, and letting “the Thread that calls poll.poll(...)” sleep while waiting on “is the socket ready.”
This episode we only need to meet two of its pieces:
mio::Poll: a place where you can “sleep waiting for I/O events.” After oneThreadregisters many I/O sources with it, a singlepoll.poll(...)watches them all at once, waking whenever any shows activity.Token: an event source’s “name tag.” When registering an I/O source, you give it aToken; later, whenPollnotifies you “there’s an event,” it hands thatTokenback, so you know which source is calling.
Watching a TcpListener with mio
The example below registers a TcpListener (the thing that accepts connections) with a Poll, then opens another Thread that connects to it after one second. The main Thread sleeps on poll.poll() and wakes when the listener reports readiness:
extern crate mio;
use mio::net::TcpListener;
use mio::{Events, Interest, Poll, Token};
use std::time::Duration;
// the listener's name tag
const SERVER: Token = Token(0);
fn main() {
let mut poll = Poll::new().expect("Poll creation failed");
let mut events = Events::with_capacity(128); // receive at most 128 events at a time
let addr = "127.0.0.1:8080".parse().expect("failed to parse the address");
let mut listener = TcpListener::bind(addr).expect("bind failed");
// register the listener with the Poll: name tag SERVER, interested in "readable" events
// (someone connecting counts as readable)
poll.registry()
.register(&mut listener, SERVER, Interest::READABLE)
.expect("register failed");
// another thread connects after one second
std::thread::spawn(|| {
std::thread::sleep(Duration::from_secs(1));
let _ = std::net::TcpStream::connect("127.0.0.1:8080");
});
println!("sleeping on poll, waiting for I/O events…");
loop {
// poll sleeps here until a registered source has an event
poll.poll(&mut events, None).expect("poll failed");
for event in events.iter() {
match event.token() {
SERVER => {
// token matches: the listener reported readable, so try accepting
match listener.accept() {
Ok((_stream, addr)) => {
println!("someone connected: {}", addr);
return; // it's an example, so call it a day
}
// readiness events may be spurious; just wait for the next one
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("accept failed: {}", e),
}
}
_ => {}
}
}
}
}
Walking the Flow
Poll::new()makes aPoll.registry().register(&mut listener, SERVER, Interest::READABLE)registers thelistener, gives it the name tagSERVER, and states our interest — “readable” (Interest::READABLE). To wait on “writable,” useInterest::WRITABLE.poll.poll(&mut events, None)puts thisThreadto sleep until a registered source has an event (Nonemeans no timeout — sleep until something happens).- On waking, check the
eventsone by one.event.token()hands back the name tag from registration; matchingSERVERtells us thelistenerreported readiness, so we tryaccept():- Success means we really got a connection.
WouldBlockmeans it still can’t accept one right now, so we go back to waiting for the next event. Readiness notifications may be spurious, so this is normal rather than a failure.- Other errors mean something actually went wrong; this simplified example panics.
mio’s sockets are non-blocking: calling accept or read does not make the Thread wait for a connection or data. If the operation can’t proceed yet, it returns immediately with WouldBlock. In this episode that means going back to poll.poll(); next episode, after we wrap I/O in a Future, the same situation maps to Poll::Pending.
The crux: even if you register a hundred I/O sources, only one Thread sleeps on that same poll.poll(). Whichever source acts up, Poll hands you its name tag. This is exactly the secret weapon a reactor uses to watch masses of I/O with just a few Threads.
Next episode, we hook mio up to the runtime we hand-wrote earlier, building a real reactor — the first time our runtime can handle real network I/O.
Recap
- A runtime has two roles: the executor runs
Tasks (poll), the reactor waits for events (watching I/O andwakeing the rightTask); the reactor isn’t aTaskand doesn’tpollFutures. mioitself is not anasyncruntime: it only does event notification for non-blocking I/O — noFutures, no.await, noTaskscheduling.mio::Pollis the “sleep waiting for I/O events” place; oneThreadcan watch many I/O sources at once.- A
Tokenis a source’s name tag: you give it at registration, andPollreturns it when the event fires so you can identify the source. - Register with
registry().register(&mut source, token, Interest::READABLE), sleep onpoll.poll(), identify byevent.token(), then try the I/O operation. If it returnsWouldBlock, wait for another event; inside aFuture, that maps toPoll::Pending.