Rust’s Journey to Async/Await
Steve Klabnik
InfoQ.com: News & Community Site
• 750,000 unique visitors/month
• Published in 4 languages (English, Chinese, Japanese and Brazilian
Portuguese)
• Post content from our QCon conferences
• News 15-20 / week
• Articles 3-4 / week
• Presentations (videos) 12-15 / week
• Interviews 2-3 / week
• Books 1 / month
Watch the video with slide
synchronization on InfoQ.com!
https://www.infoq.com/presentations/
rust-2019/
Presented at QCon New York
www.qconnewyork.com
Purpose of QCon
- to empower software development by facilitating the spread of
knowledge and innovation
Strategy
- practitioner-driven conference designed for YOU: influencers of
change and innovation in your teams
- speakers and topics driving the evolution and innovation
- connecting and catalyzing the influencers and innovators
Highlights
- attended by more than 12,000 delegates since 2007
- held in 9 cities worldwide
Hi, I’m Steve!
● On the Rust
team
● Work at
Cloudflare
● Doing two
workshops!
What is async?
Parallel: do multiple things at
once
Concurrent: do multiple
things, not at once
Asynchronous: actually
unrelated! Sort of...
“Task”
A generic term for some
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
Asynchronous
A word we use to describe
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:
● Part of your system; OS handles
scheduling
● Very straightforward,
well-understood
Native thread disadvantages:
● Defaults can be sort of heavy
● Relatively limited number you can
create
Green thread advantages:
● Not part of the overall system;
runtime handles scheduling
● Lighter weight, can create many,
many, many, many green threads
Green thread disadvantages:
● Stack growth can cause issues
● Overhead when calling into C
Why do we care?
Apache
“Pre-fork”
Control Process
Child Process
Apache
“worker”
Control Process
Child Process
Child Thread Child Thread
Child Thread Child Thread
Thread pool
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
● Your code looks like it blocks, and
it does block
● Very basic and straightforward
Asynchronous, non-blocking
● Your code looks like it doesn’t
block, and it doesn’t block
● Harder to write
Synchronous, non-blocking
● Your code looks like it blocks, but it
doesn’t!
● The secret: the runtime is
non-blocking
● Your code still looks straightforward,
but you get performance benefits
● A common path for languages built
on synchronous, blocking I/O to gain
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
CPU Bound
The speed of completing a task
is based on the CPU crunching
some numbers
My processor is working hard
I/O Bound
The speed of completing a task
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
Go
Asynchronous I/O
with green threads
(Erlang does this too)
Main Process
Child Thread Child Thread
Child Thread Child Thread
Green threads
Native vs Green threads
Native thread advantages:
● Part of your system; OS handles
scheduling
● Very straightforward,
well-understood
Native thread disadvantages:
● Defaults can be sort of heavy
● Relatively limited number you can
create
Green thread advantages:
● Not part of the overall system;
runtime handles scheduling
● Lighter weight, can create many,
many, many, many green threads
Green thread disadvantages:
● Stack growth can cause issues
● Overhead when calling into C
PREVIOUSLY
A “systems
programming language”
that has overhead when
calling into C code?
Luckily, there is
another way
Nginx
Asynchronous I/O
Event Loop
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
● The language has a runtime
● This means that Promises start
executing upon creation
● This feels simpler, but has some
drawbacks, namely, lots of
allocations
● Futures are not built into Rust
● The language has no runtime
● This means that you must submit
your futures to an executor to start
execution
● Futures are inert until their poll
method is called by the executor
● 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.
// 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
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)?);
We argued and
argued and
argued and
argued and
argued and ar...
What syntax for async/await?
async {
let mut buf = [0; 1024];
let mut cursor = 0;
while cursor < 1024 {
cursor += socket.read(&mut buf[cursor..]).await?;
};
buf
}
// no errors
future.await
// with errors
future.await?
… there’s actually
even one last
issue that’s
popped up
… this talk is
already long
enough
Additional Ergonomic improvements
use runtime::net::UdpSocket;
#[runtime::main]
async fn main() -> std::io::Result<()> {
let mut socket = UdpSocket::bind("127.0.0.1:8080")?;
let mut buf = vec![0u8; 1024];
println!("Listening on {}", socket.local_addr()?);
loop {
let (recv, peer) = socket.recv_from(&mut buf).await?;
let sent = socket.send_to(&buf[..recv], &peer).await?;
println!("Sent {} out of {} bytes to {}", sent, recv, peer);
}
}
WebAssembly?
WebAssembly?
Promise
Future
Promise
Finally landing in
Rust 1.37
Or maybe 1.38
Finally landing in
Rust 1.37
Or maybe 1.38
Finally landing in
Rust 1.38!!!!1
Lesson: a
world-class
I/O system
implementation
takes years
Lesson: different
languages have
different
constraints
Thank you!
@steveklabnik
Watch the video with slide
synchronization on InfoQ.com!
https://www.infoq.com/presentations/
rust-2019/

Rust's Journey to Async/await

  • 1.
    Rust’s Journey toAsync/Await Steve Klabnik
  • 2.
    InfoQ.com: News &Community Site • 750,000 unique visitors/month • Published in 4 languages (English, Chinese, Japanese and Brazilian Portuguese) • Post content from our QCon conferences • News 15-20 / week • Articles 3-4 / week • Presentations (videos) 12-15 / week • Interviews 2-3 / week • Books 1 / month Watch the video with slide synchronization on InfoQ.com! https://www.infoq.com/presentations/ rust-2019/
  • 3.
    Presented at QConNew York www.qconnewyork.com Purpose of QCon - to empower software development by facilitating the spread of knowledge and innovation Strategy - practitioner-driven conference designed for YOU: influencers of change and innovation in your teams - speakers and topics driving the evolution and innovation - connecting and catalyzing the influencers and innovators Highlights - attended by more than 12,000 delegates since 2007 - held in 9 cities worldwide
  • 4.
    Hi, I’m Steve! ●On the Rust team ● Work at Cloudflare ● Doing two workshops!
  • 6.
    What is async? Parallel:do multiple things at once Concurrent: do multiple things, not at once Asynchronous: actually unrelated! Sort of...
  • 7.
    “Task” A generic termfor some computation running in a parallel or concurrent system
  • 8.
    Parallel Only possible withmultiple cores or CPUs
  • 9.
    Concurrent Pretend that youhave multiple cores or CPUs
  • 10.
    Asynchronous A word weuse to describe language features that enable parallelism and/or concurrency
  • 11.
  • 12.
  • 13.
    Cooperative Multitasking Each taskdecides when to yield to other tasks
  • 14.
    Preemptive Multitasking The systemdecides when to yield to other tasks
  • 15.
  • 16.
    Native threads Tasksprovided by the operating system Sometimes called “1:1 threading”
  • 17.
    Green Threads Tasksprovided by your programming language Sometimes called “N:M threading”
  • 18.
    Native vs Greenthreads Native thread advantages: ● Part of your system; OS handles scheduling ● Very straightforward, well-understood Native thread disadvantages: ● Defaults can be sort of heavy ● Relatively limited number you can create Green thread advantages: ● Not part of the overall system; runtime handles scheduling ● Lighter weight, can create many, many, many, many green threads Green thread disadvantages: ● Stack growth can cause issues ● Overhead when calling into C
  • 19.
    Why do wecare?
  • 21.
  • 22.
    Apache “worker” Control Process Child Process ChildThread Child Thread Child Thread Child Thread Thread pool
  • 23.
  • 24.
    Rust was builtto enhance Firefox, which is an HTTP client, not server
  • 27.
  • 28.
  • 29.
    Synchronous Asynchronous Blocking Old-schoolimplementations Doesn’t make sense Non-blocking Go, Ruby Node.js
  • 30.
    Tons of options Synchronous,blocking ● Your code looks like it blocks, and it does block ● Very basic and straightforward Asynchronous, non-blocking ● Your code looks like it doesn’t block, and it doesn’t block ● Harder to write Synchronous, non-blocking ● Your code looks like it blocks, but it doesn’t! ● The secret: the runtime is non-blocking ● Your code still looks straightforward, but you get performance benefits ● A common path for languages built on synchronous, blocking I/O to gain performance while retaining compatibility
  • 31.
    Not all waswell in Rust-land
  • 33.
    A “systems programming language” thatdoesn’t let you use the system’s threads?
  • 37.
    Not all waswell in Rust-land
  • 39.
  • 40.
    Ship the minimal thingthat we know is good
  • 41.
  • 42.
    … but still,not all was well in Rust-land
  • 43.
  • 44.
    People want to buildnetwork services in Rust
  • 45.
    Rust is supposed tobe a high-performance language
  • 46.
    Rust’s I/O model feelsretro, and not performant
  • 47.
    The big problem withnative threads for I/O
  • 48.
  • 49.
    CPU Bound The speedof completing a task is based on the CPU crunching some numbers My processor is working hard
  • 50.
    I/O Bound The speedof completing a task is based on doing a lot of input and output Doing a lot of networking
  • 51.
    When you’re doing alot of I/O, you’re doing a lot of waiting
  • 52.
    When you’re doing alot of waiting, you’re tying up system resources
  • 53.
    Go Asynchronous I/O with greenthreads (Erlang does this too) Main Process Child Thread Child Thread Child Thread Child Thread Green threads
  • 54.
    Native vs Greenthreads Native thread advantages: ● Part of your system; OS handles scheduling ● Very straightforward, well-understood Native thread disadvantages: ● Defaults can be sort of heavy ● Relatively limited number you can create Green thread advantages: ● Not part of the overall system; runtime handles scheduling ● Lighter weight, can create many, many, many, many green threads Green thread disadvantages: ● Stack growth can cause issues ● Overhead when calling into C PREVIOUSLY
  • 55.
    A “systems programming language” thathas overhead when calling into C code?
  • 56.
  • 57.
  • 59.
  • 60.
  • 61.
  • 64.
    Promises let myFirstPromise =new Promise((resolve, reject) => { setTimeout(function(){ resolve("Success!"); }, 250); }); myFirstPromise.then((successMessage) => { console.log("Yay! " + successMessage); });
  • 65.
    Promises let myFirstPromise =new Promise((resolve, reject) => { setTimeout(function(){ resolve("Success!"); }, 250); }); myFirstPromise.then((successMessage) => { console.log("Yay! " + successMessage); }).then((...) => { // }).then((...) => { // });
  • 68.
    Futures 0.1 pub traitFuture { 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) })
  • 69.
    Promises and Futuresare different! ● Promises are built into JavaScript ● The language has a runtime ● This means that Promises start executing upon creation ● This feels simpler, but has some drawbacks, namely, lots of allocations ● Futures are not built into Rust ● The language has no runtime ● This means that you must submit your futures to an executor to start execution ● Futures are inert until their poll method is called by the executor ● 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
  • 70.
    Futures 0.1: Executors usetokio; 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); }
  • 71.
    We used Futures 0.1to build stuff!
  • 72.
  • 73.
    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.
  • 76.
    // 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
  • 77.
    Async/await lets you writecode that feels synchronous, but is actually asynchronous
  • 78.
    Async/await is more importantin Rust than in other languages because Rust has no garbage collector
  • 79.
    Rust example: synchronous fnread(&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..])?; }
  • 80.
    Rust example: asyncwith 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.
  • 81.
    Rust example: asyncwith 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.
  • 82.
    Not all futurescan error trait Future { type Item; type Error; fn poll(&mut self, cx: task::Context) -> Poll<Self::Item, Self::Error>; }
  • 83.
    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>.
  • 84.
    … but onemore thing...
  • 85.
    What syntax forasync/await? async is not an issue JavaScript and C# do: await value; But what about ? for error handling? await value?; await (value?); (await value)?;
  • 86.
    What syntax forasync/await? What about chains of await? (await (await value)?);
  • 87.
    We argued and arguedand argued and argued and argued and ar...
  • 89.
    What syntax forasync/await? async { let mut buf = [0; 1024]; let mut cursor = 0; while cursor < 1024 { cursor += socket.read(&mut buf[cursor..]).await?; }; buf } // no errors future.await // with errors future.await?
  • 90.
    … there’s actually evenone last issue that’s popped up
  • 91.
    … this talkis already long enough
  • 92.
    Additional Ergonomic improvements useruntime::net::UdpSocket; #[runtime::main] async fn main() -> std::io::Result<()> { let mut socket = UdpSocket::bind("127.0.0.1:8080")?; let mut buf = vec![0u8; 1024]; println!("Listening on {}", socket.local_addr()?); loop { let (recv, peer) = socket.recv_from(&mut buf).await?; let sent = socket.send_to(&buf[..recv], &peer).await?; println!("Sent {} out of {} bytes to {}", sent, recv, peer); } }
  • 93.
  • 94.
  • 95.
    Finally landing in Rust1.37 Or maybe 1.38
  • 96.
    Finally landing in Rust1.37 Or maybe 1.38
  • 98.
  • 100.
  • 101.
  • 102.
  • 103.
    Watch the videowith slide synchronization on InfoQ.com! https://www.infoq.com/presentations/ rust-2019/