On blocking I/O: What is the cost of blocking?
Instead of full threads, we could use lightweight threads that can be swapped out for each other. However they need a runtime.
How to fix this? Async await:
fn main() {
let listener = TcpListener::bind("127.0.0.1::25565").unwrap();
for stream_res in listener.incoming() {
let mut stream = stream_res.unwrap();
thread::spawn(move || {
let mut str = String::new();
stream.read_to_string(&mut str).unwrap(); // WHEN THE read() SYSTEM CALL BLOCKS, THE ENTIRE THING BLOCKS
});
}
}
Resolution with epoll: a linux kernel mechanism for polling file descriptors that are ready for I/O.
while true {
ready_ids = epoll() // POLLS ALL THE READY FDs
for each id in ready_ids {
read(id)
}
}
State of each thread needs to be managed. e.g.
A Future allows us to keep track of a in-progress operation together
with the state in one package.
Futures are also a zero-cost abstraction. The binary code has no allocation and no runtime overhead
async/.await in rustasync fn func() { }
// The above async fn is syntactic sugar for
fn func() -> impl Future<Output=()> { }
// The below is definition of the Future trait
trait Future {
type Output;
fn poll(&mut self, cs: &mut Context) -> Poll<Self::Output>;
// Context includes the wake() function, as shown with the below alternative params
// fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll {
Ready(T),
Pending,
}
Futures in Rust are lazy so they will not do anything until they are polled i.e. when await is called.
Polling can only occur inside an async function (i.e. await) can only be called within async fn.
Then who polls the main function?
Note that waiting $\neq$ blocking!
poll() called on the first future main.await.awaited future until it cannot progress
Poll::Ready(T)Poll::Pending.wake() is called BY ONE OR MORE of the futures
waits.Context to see which Future can be polled.Future trait: poll(context)reactor: Event loop executor + a Context
when poll is called, a Context structure is passed in, which includes a wake() function.
An executor loops over futures until some thread makes progress.
Tokio: mio.rs + futures.rs
executors can be single/multi threaded
Futures are combined with combinators e.g. and_then
Poor ergonomics:
However syntactic sugar introduced:
async and await?async fn returns a futuretokio)The Future returned by an async function needs to have a fixed size known at compile time.
Rust async functions are nearly optimal in terms of memory usage and allocations (zero cost abstraction).