As announced on the Rust Blog a few weeks ago, the long awaited async-await syntax hit beta and is slated for release with 1.39 early November. Take a look at the Async Book for an in-depth introduction.
In eager anticipation of async-await, we’ve been using futures 0.1 crate for some time. But, we’re long overdue to finally take a look at where things are headed. The following is some loosely structured notes on the upcoming “stable” state of futures and async/await.
Foot in the Door
cargo new async
and in Cargo.toml
add:
[dependencies] futures = { version = "0.3.0-alpha", package = "futures-preview" }
Here using the “mostly final” Futures 0.3 crate.
As explained in The Book there are three release channels: nightly, beta, and stable. You may have used “nightly” before to access the latest and greatest features- this is where async/await previously lived. “beta” is where a version goes before becoming the next “stable” release.
# Update all installed toolchains rustup update # List installed toolchains rustup toolchain list # Install beta toolchain rustup install beta
There’s a few ways to use the beta/nightly channels:
# List toolchain overrides rustup override list # Set default toolchain for directory rustup override set beta # or "nightly" # Now defaults to beta toolchain cargo build # Explicitly build using beta/nightly toolchain cargo +beta build # or "+nightly"
In src/main.rs
:
async fn hello_world() { println!("Hello world"); } async fn start() { hello_world().await } fn main() { let future = start(); futures::executor::block_on(future); }
cargo run
to greet the world.
Notice how await
is post-fix instead of await hello_world()
as found in many other languages. The syntax was heavily debated, but the rationale boils down to improving: method chaining, co-existance with the ?
operator, and precedence rules.
A contrived example with a series of calls (some of which can fail):
let answer = can_fail().await? .some_func().await .member_can_fail().await? .get_answer()?;
You can’t understand async
without understanding Rust’s Future
trait. Perhaps the first thing to learn about Future
is they’re lazy; nothing happens unless something “pumps” them- as executor::block_on
does here. Contrast this with std::thread::spawn
which creates a running thread. If futures are polled, does that mean Rust async programming isn’t event-driven à la epoll/kqueue? Don’t fret, a Waker
can be used to signal the future is ready to be polled again.
Error-Handling
We have test code like:
while !done.load(Ordering::Relaxed) { match block_on(ctx.receive()) { Ok(msg) => { let pipe = msg.get_pipe()?; let mut response = msg.dup()?; response.set_pipe(&pipe); block_on(ctx.send(response))?; } _ => panic!(), } }
How we might re-write it:
let future = async { while !done.load(Ordering::Relaxed) { match ctx.receive().await { Ok(msg) => { let pipe = msg.get_pipe()?; let mut response = msg.dup()?; response.set_pipe(&pipe); ctx.send(response).await?; } _ => panic!(), } } }; block_on(future);
Unfortunately, how the ?
operator works in async blocks (i.e. async {}
) is not defined, and async closures (i.e. async || {}
) are unstable.
If we replace ?
with .unwrap()
it compiles and runs.
Heterogeneous Returns
Given:
broker_pull_ctx.receive().for_each(|msg| { if let Ok(msg) = msg { broker_push_ctx.send(msg).then(|msg| { // Do something with the message future::ready(()) }) } else { future::ready(()) } });
Sadness:
| 144 | / if let Ok(msg) = msg { 145 | | broker_push_ctx.send(msg).then(|res| { | | _____________________ - 146 | || res.unwrap().unwrap(); 147 | || future::ready(()) 148 | || }) | || ______________________ - expected because of this 149 | | } else { 150 | | future::ready(()) | | ^^^^^^^^^^^^^^^^^ expected struct `futures_util::future::then::Then`, found struct `futures_util::future::ready::Ready` 151 | | } | | _________________ - if and else have incompatible types | = note: expected type `futures_util::future::then::Then<futures_channel::oneshot::Receiver<std::result::Result<(), runng::result::Error>>, futures_util::future::ready::Ready<_>, [closure@runng/tests/test_main.rs:145:52: 148:22]>` found type `futures_util::future::ready::Ready<_>`
Basically, then()
- like many Rust combinators- returns a distinct type (Then
in this case).
If you reach for a trait object for type erasure via -> Box<dyn Future<Output = ()>>
and wrap the returns in Box::new()
you’ll run into:
error[E0277]: the trait bound `dyn core::future::future::Future<Output = ()>: std::marker::Unpin` is not satisfied --> runng/tests/test_main.rs:155:58 | 155 | let fut = broker_pull_ctx.receive().unwrap().for_each(|msg| -> Box<dyn Future<Output = ()>> { | ^^^^^^^^ the trait `std::marker::Unpin` is not implemented for `dyn core::future::future::Future<Output = ()>` | = note: required because of the requirements on the impl of `core::future::future::Future` for `std::boxed::Box<dyn core::future::future::Future<Output = ()>>`
Lo, the 1.33 feature “pinning”. Thankfully, the type-insanity that is Pin<Box<dyn Future<Output = T>>>
is common enough that a future::BoxFuture<T>
alias is provided:
let fut = broker_pull_ctx...for_each(|msg| -> future::BoxFuture<()> { if let Ok(msg) = msg { Box::pin(broker_push_ctx.send(msg).then(|_| { })) } else { Box::pin(future::ready(())) } }); block_on(fut);
Alternatively, you can multiplex the return with something like future::Either
:
let fut = broker_pull_ctx...for_each(|msg| { use futures::future::Either; if let Ok(msg) = msg { Either::Left( broker_push_ctx.send(msg).then(|_| { }) ) } else { Either::Right(future::ready(())) } }); block_on(fut);
This avoids the boxing allocation, but it might become a bit gnarly if there’s a large number of return types.
block_on()
!= .await
Our initial, exploratory implementation made heavy use of wait()
found in futures 0.1. To transition to async/await it’s tempting to replace wait()
with block_on()
:
#[test] fn block_on_panic() -> runng::Result<()> { let url = get_url(); let mut rep_socket = protocol::Rep0::open()?; let mut rep_ctx = rep_socket.listen(&url)?.create_async()?; let fut = async { block_on(rep_ctx.receive()).unwrap(); }; block_on(fut); Ok(()) }
cargo test block_on_panic
yields:
---- tests::reqrep_tests::block_on_panic stdout ---- thread 'tests::reqrep_tests::block_on_panic' panicked at 'cannot execute `LocalPool` executor from within another executor: EnterError', src/libcore/result.rs:1165:5
Note this isn’t a compiler error, it’s a runtime panic. I haven’t looked into the details of this, but the problem stems from the nested calls to block_on()
. It seems that if the inner future finishes immediately everything is fine, but not if it blocks. However, it works as expected with await:
let fut = async { rep_ctx.receive().await.unwrap(); }; block_on(fut);
Async Traits
Try:
trait AsyncTrait { async fn do_stuff(); }
Nope:
error[E0706]: trait fns cannot be declared `async` --> src/main.rs:5:5 | 5 | async fn do_stuff(); | ^^^^^^^^^^^^^^^^^^^^
How about:
trait AsyncTrait { fn do_stuff(); } struct Type; impl AsyncTrait for Type { async fn do_stuff() { } }
Nope:
error[E0706]: trait fns cannot be declared `async` --> src/main.rs:11:5 | 11 | async fn do_stuff() { } | ^^^^^^^^^^^^^^^^^^^^^^^
One work-around involves being explicit about the return types, and using an async block within the impl:
use futures::future::{self, BoxFuture, Future}; trait AsyncTrait { fn boxed_trait() -> Box<dyn Future<Output = ()>>; fn pinned_box() -> BoxFuture<'static, ()>; } impl<T> AsyncTrait for T { fn boxed_trait() -> Box<dyn Future<Output = ()>> { Box::new(async { // .await to your heart's content }) } fn pinned_box() -> BoxFuture<'static, ()> { Box::pin(async { // .await to your heart's content }) } }
Top comments (3)
Nice writeup. I believe the Rust team and community drew a lot of inspiration from the C# async-await, during their planning and design stages? Good to see a lot of other programming languages incorporating this feature into their various ecosystems. Once more, we'll done Rust.
From the various threads I've read they look at a wide range of "prior art", and it was the main argument against postfix ".await"- it would be foreign to everyone.
Should have mentioned this is just the MVP to unblock the ecosystem and async-await is far from "finished". Should probably sneak that into the opening somewhere.
Cool.