Asynchronous Rust
Asynchronous Rust is a collection of features in the Rust programming language, which allow you to write applications that can perform many tasks at the same time without spawning a thread for every task. These features are targeted at tasks that spend most of their time waiting for other things such as timers or network connections. The basic idea behind the implementation is that when a task needs to wait for something, the task can put itself to sleep in a way that allows other tasks to run on the same thread while it is sleeping.
The basic building block of asynchronous Rust is the Future
trait, which defines what an asynchronous task is. The current state of each asynchronous task is stored as a future object. A future is executed by repeatedly calling the poll
method, which makes the future continue execution until it reaches the next natural place to sleep, or until it has terminated.
The standard library does not provide any functionality for executing futures, so you will need an external runtime crate to execute your futures. The async crate comparison page lists the currently available runtimes.
The async/await feature
Although it is possible to create a future by manually implementing the Future
trait on your own type, this is rarely necessary. The Rust programming language supports a certain kind of function known as an async function, which will automatically turn imperative code into a future object. The future considers each use of .await
a natural place to sleep. The Rust language provides the async book to teach these concepts, although it is not as polished as the Rust book.
Blocking the thread
Asynchronous Rust uses cooperative scheduling to run many things simultaneously on a few threads. The runtime can run many things simultaneously by alternating which future it calls the poll
method on. This has the consequence that futures must cooperate with each other — if one spends a long time inside the call to poll
, all other futures are unable to continue working for that duration.
If a future spends a long time inside the call to poll
, this is called blocking the thread. On the other hand, if a future waits for something by returning from poll
, it is called yielding. The classic example of blocking is the sleep
function from the standard library:
// This is an example of blocking the thread. async fn example() { std::thread::sleep(Duration::from_secs(5)); } // This is an example of yielding. async fn example() { tokio::time::delay_for(Duration::from_secs(5)).await; }
The first example above will block the thread for five seconds, whereas the second example does not block the thread at all. A good rule of thumb is that if an async function does not reach an .await
for an extended period of time, it is blocking the thread.
Most runtimes provide a function called spawn_blocking
, which can run code that would otherwise block the runtime on a different thread pool intended for blocking operations. This will still use an entire thread for that operation, but at least it won't prevent other asynchronous tasks from running.