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
.await
ed future until it cannot progress
Poll::Ready(T)
Poll::Pending
.wake()
is called BY ONE OR MORE of the futures
wait
s.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).