Rust’s Journey to Async/Await Steve Klabnik
Hi, I’m Steve! ● On the Rust team ● Work at Cloudflare ● Doing two workshops!
Parallel: do multiple things at once What is async? Concurrent: do multiple things, not at once Asynchronous: actually unrelated! Sort of...
A generic term for some “Task” computation running in a parallel or concurrent system
Parallel Only possible with multiple cores or CPUs
Concurrent Pretend that you have multiple cores or CPUs
A word we use to describe Asynchronous language features that enable parallelism and/or concurrency
Even more terminology
Cooperative vs Preemptive Multitasking
Cooperative Multitasking Each task decides when to yield to other tasks
Preemptive Multitasking The system decides when to yield to other tasks
Native vs green threads
Native threads Tasks provided by the operating system Sometimes called “1:1 threading”
Green Threads Tasks provided by your programming language Sometimes called “N:M threading”
Native vs Green threads Native thread advantages: Green thread advantages: Part of your system; OS handles Not part of the overall system; ● ● scheduling runtime handles scheduling Very straightforward, Lighter weight, can create many, ● ● well-understood many, many, many green threads Native thread disadvantages: Green thread disadvantages: Defaults can be sort of heavy Stack growth can cause issues ● ● Relatively limited number you can Overhead when calling into C ● ● create
Why do we care?
Control Process Apache “Pre-fork” Child Process
Control Process Child Process Apache “worker” Thread pool Child Thread Child Thread Child Thread Child Thread
Let’s talk about Rust
Rust was built to enhance Firefox, which is an HTTP client, not server
“Synchronous, non-blocking network I/O”
Isn’t this a contradiction in terms?
Synchronous Asynchronous Blocking Old-school implementations Doesn’t make sense Non-blocking Go, Ruby Node.js
Tons of options Synchronous, blocking Synchronous, non-blocking Your code looks like it blocks, but it Your code looks like it blocks, and ● ● doesn’t! it does block The secret: the runtime is ● Very basic and straightforward ● non-blocking Asynchronous, non-blocking Your code still looks straightforward, ● but you get performance benefits Your code looks like it doesn’t ● A common path for languages built ● block, and it doesn’t block on synchronous, blocking I/O to gain Harder to write ● performance while retaining compatibility
Not all was well in Rust-land
A “systems programming language” that doesn’t let you use the system’s threads?
Not all was well in Rust-land
Rust 1.0 was approaching
Ship the minimal thing that we know is good
Rust 1.0 was released! 🎊
… but still, not all was well in Rust-land
People 💗 Rust
People want to build network services in Rust
Rust is supposed to be a high-performance language
Rust’s I/O model feels retro, and not performant
The big problem with native threads for I/O
CPU bound vs I/O bound
The speed of completing a task CPU Bound is based on the CPU crunching some numbers My processor is working hard
The speed of completing a task I/O Bound is based on doing a lot of input and output Doing a lot of networking
When you’re doing a lot of I/O, you’re doing a lot of waiting
When you’re doing a lot of waiting, you’re tying up system resources
Main Process Go Green threads Asynchronous I/O Child Thread Child Thread with green threads Child Thread Child Thread (Erlang does this too)
Native vs Green threads PREVIOUSLY Native thread advantages: Green thread advantages: Part of your system; OS handles Not part of the overall system; ● ● scheduling runtime handles scheduling Very straightforward, Lighter weight, can create many, ● ● well-understood many, many, many green threads Native thread disadvantages: Green thread disadvantages: Defaults can be sort of heavy Stack growth can cause issues ● ● Relatively limited number you can Overhead when calling into C ● ● create
A “systems programming language” that has overhead when calling into C code?
Luckily, there is another way
Event Loop Nginx Asynchronous I/O
Evented I/O requires non-blocking APIs
Blocking vs non-blocking
“Callback hell”
Promises let myFirstPromise = new Promise((resolve, reject) => { setTimeout(function(){ resolve("Success!"); }, 250); }); myFirstPromise.then((successMessage) => { console.log("Yay! " + successMessage); });
Promises let myFirstPromise = new Promise((resolve, reject) => { setTimeout(function(){ resolve("Success!"); }, 250); }); myFirstPromise.then((successMessage) => { console.log("Yay! " + successMessage); }).then((...) => { // }).then((...) => { // });
Futures 0.1 pub trait Future { type Item; type Error; fn poll(&mut self) -> Poll<Self::Item, Self::Error>; } id_rpc(&my_server).and_then(|id| { get_row(id) }).map(|row| { json::encode(row) }).and_then(|encoded| { write_string(my_socket, encoded) })
Promises and Futures are different! Promises are built into JavaScript Futures are not built into Rust ● ● The language has no runtime The language has a runtime ● ● This means that you must submit ● This means that Promises start ● your futures to an executor to start executing upon creation execution This feels simpler, but has some ● Futures are inert until their poll ● drawbacks, namely, lots of method is called by the executor allocations This is slightly more complex, but ● extremely efficient; a single, perfectly sized allocation per task! Compiles into the state machine ● you’d write by hand with evented I/O
Futures 0.1: Executors use tokio; fn main() { let addr = "127.0.0.1:6142".parse().unwrap(); let listener = TcpListener::bind(&addr).unwrap(); let server = listener.incoming().for_each(|socket| { Ok(()) }) .map_err(|err| { println!("accept error = {:?}", err); }); println!("server running on localhost:6142"); tokio::run(server); }
We used Futures 0.1 to build stuff!
The design had some problems
Futures 0.2 trait Future { type Item; type Error; fn poll(&mut self, cx: task::Context) -> Poll<Self::Item, Self::Error>; } No implicit context, no more need for thread local storage.
Async/await // with callback request('https://google.com/', (response) => { // handle response }) // with promise request('https://google.com/').then((response) => { // handle response }); // with async/await async function handler() { let response = await request('https://google.com/') // handle response }
Async/await lets you write code that feels synchronous, but is actually asynchronous
Async/await is more important in Rust than in other languages because Rust has no garbage collector
Rust example: synchronous fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> let mut buf = [0; 1024]; let mut cursor = 0; while cursor < 1024 { cursor += socket.read(&mut buf[cursor..])?; }
Rust example: async with Futures fn read<T: AsMut<[u8]>>(self, buf: T) -> impl Future<Item = (Self, T, usize), Error = (Self, T, io::Error)> … the code is too big to fit on the slide The main problem: the borrow checker doesn’t understand asynchronous code. The constraints on the code when it’s created and when it executes are different.
Rust example: async with async/await async { let mut buf = [0; 1024]; let mut cursor = 0; while cursor < 1024 { cursor += socket.read(&mut buf[cursor..]).await?; }; buf } async/await can teach the borrow checker about these constraints.
Not all futures can error trait Future { type Item; type Error; fn poll(&mut self, cx: task::Context) -> Poll<Self::Item, Self::Error>; }
std::future pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>; } Pin is how async/await teaches the borrow checker. If you need a future that errors, set Output to a Result<T, E> .
… but one more thing...
What syntax for async/await? async is not an issue JavaScript and C# do: await value; But what about ? for error handling? await value?; await (value?); (await value)?;
What syntax for async/await? What about chains of await ? (await (await value)?);
Recommend
More recommend