Unlocking the Power of Async in Rust
Rust’s async story has been unfolding rapidly, and with the stabilization of the async/await syntax, things are finally coming together. But async in Rust works differently than in other languages, such as JavaScript. One major difference is that async in Rust requires an executor to work, which isn’t even available in the standard library. In this tutorial, we’ll explore three Rust async packages, evaluate their production-readiness, and demonstrate how to build fast, reliable, and highly concurrent applications.
How Async Works in Rust
At its core, async/await in Rust is built on top of Futures. If you’re coming from JavaScript, you can think of futures like promises: they’re values that haven’t finished computing yet. Unlike promises, however, futures won’t make any progress until they are explicitly polled. This is where executors come in. An executor is a runtime that manages futures for you by polling them when they’re ready to make progress.
Tokio: The Most Popular Async Crate
Tokio is the most popular crate for dealing with async Rust. In addition to an executor, Tokio provides async versions of many standard library types. Much of the functionality in this crate is behind optional features that you’ll need to enable. This helps to keep compile time and binary size down when the features aren’t needed.
We’ll use the “full” feature to enable them all. Now you can spawn a Tokio runtime and give it a future to run. Our application code could go inside the async block in the block_on
method. This boilerplate can be replaced with a macro that lets you use an async main function instead.
Blocking Code and Green Threads
Using sleep also affords us a good opportunity to look at what happens when you block in an async context without using await. If you run this now, you’ll see that all futures are blocked from making progress as long as the second future is blocked. Since we didn’t use await, the second future can’t know to yield and give control back to the Tokio runtime.
Fortunately, Tokio has our back here. The tokio::task
module contains an implementation of green threads, similar to Go’s goroutines. With spawn_blocking
, you can get the Tokio runtime to run blocking code inside a dedicated thread pool, allowing other futures to continue making progress.
Tokio Tasks and Async Workers
To take full advantage of futures and async/await, let’s use asynchronous code from top to bottom. We’ll spawn futures into their own background task using tokio::task::spawn
, an async version of std::thread::spawn
. We’ll test this out by making a pool of async workers that can receive jobs to run in the background.
Async-std: An Alternative to Tokio
Async-std attempts to be an asynchronous version of the Rust standard library. It has similar goals as Tokio, which is its main competitor, but is less popular and therefore less battle-tested in the wild. While the APIs of Tokio and async-std aren’t identical, they are fairly similar and most concepts transfer between the two.
Futures: The Foundation of Async in Rust
The futures-rs crate provides much of the shared foundational pieces for async in Rust. Both Tokio and async-std use parts of the futures crate for things like shared traits. In fact, std::future::Future
was originally taken from this crate and other parts will likely be moved into the standard library at some point.
The Future of Async in Rust
In my opinion, async Rust is in a great place despite some concerns around runtime compatibility. Tokio and async-std are both general-purpose async runtimes that provide async alternatives to standard library types. For a production application, I’d currently recommend Tokio due to its maturity and the number of crates built on top of it.
For people looking for a batteries-included framework, Rust isn’t a good choice yet. But for those that prefer building applications out of smaller, more modular pieces, Tokio and its surrounding ecosystem are brilliant.