Async
Recipe | Crates | Categories |
---|---|---|
Basic example | ||
Differences with other languages | ||
Which crate provides what? | ||
Async runtimes |
Asynchronous programming, or async for short, is a concurrent programming model supported by an increasing number of programming languages. It lets you run a large number of concurrent tasks, while preserving much of the look and feel of ordinary synchronous programming, through the async/await syntax. It helps you deal with events independently of the main program flow, using techniques like futures, promises, waiting, or eventing.
- Ability to make progress on multiple tasks, even if they don't execute at the exact same time.
- Mechanism: cooperative multitasking - tasks yield control, allowing other tasks to run.
- Involves context switching on a single thread or, most often, among a few threads (the pool of which is opaquely managed by the async runtime).
- Achieves non-blocking I/O operations to improve responsiveness and efficiency.
- Lower overhead compared to multithreading.
- Multi-threaded async programming also requires careful synchronization to prevent data races.
Key constructs in Rust:
async
⮳ /await
⮳ keywordsstd::future::Future
⮳
Basic example
use std::future::Future; struct SomeStruct; // Most often, we will use async functions. // Rust transforms the `async fn` at compile time into a state machine // that _implicitly_ returns a `Future`. A future represents an // asynchronous computation that might not have finished yet. async fn first_task() -> SomeStruct { // ... println!("First task"); SomeStruct } async fn second_task_1(_s: &SomeStruct) { // ... println!("Second task, part 1"); } // `async fn` is really syntaxic sugar for a function... #[allow(clippy::manual_async_fn)] fn second_task_2() -> impl Future<Output = ()> { // ...that contains an `async` block. async { println!("Second task, part 2"); } // returns `Future<Output = ()>` } async fn do_something() { // Use `.await` to start executing the future. let s = first_task().await; // `await` yields control back to the executor, which may decide to do // other work if the task is not ready, then come back here. // `join!` is like `.await` but can wait for multiple futures // concurrently, returning when all branches complete. let f1 = second_task_1(&s); let f2 = second_task_2(); futures::join!(f1, f2); // or tokio::join! } // We replace `fn main()` by `async fn main()` and declare which // executor runtime we'll use - in this case, Tokio. The runtime crate // must be added to `Cargo.toml`: `tokio = { version = "1", features = // that transforms it into a synchronous fn main() that initializes a // runtime instance and executes the async main function. #[tokio::main] async fn main() { do_something().await; // note: `await` must be called or nothing is executing. // Futures are lazy }
As any form of cooperative multitasking, a future that spends a long time without reaching an await
⮳ "blocks the thread", which may prevent other tasks from running.
Differences with other languages
Rust's implementation of async
⮳ differs from most languages in a few ways:
- Rust's
async
⮳ operations are lazy. Futures are inert in Rust and only make progress only when polled. The executor calls thestd::task::Poll
⮳ method repeatedly to execute futures.
async fn say_world() { println!("world"); } #[tokio::main] async fn main() { // Calling `say_world()` does not execute the body of `say_world()`. let op = say_world(); // This println! comes first println!("hello"); // Calling `.await` on `op` starts executing `say_world`. op.await; } // Prints: // hello // world // Example from https://tokio.rs/tokio/tutorial/hello-tokio
- Dropping a future stops it from making further progress.
- Async is zero-cost in Rust. You can use
async
⮳ without heap allocations and dynamic dispatch. This also lets you use async in constrained environments, such as embedded systems. - No built-in runtime is provided by Rust itself. Instead, runtimes are provided by community-maintained crates.
- Both single- and multi-threaded runtimes are available.
Which crate provides what?
- The
async
⮳ /await
⮳ syntactic sugar is supported directly by the Rust compiler. - The most fundamental traits, types, and functions, such as the
std::future::Future
⮳ trait, are provided by the standard library. - Many utility types, macros and functions are provided by the
futures
⮳ crate. They can be used in any async Rust application. - Execution of async code, IO and task spawning are provided by "async runtimes", such as
tokio
⮳ andasync_std
⮳. Most async applications, and some async crates, depend on a specific runtime.
Async runtimes
In most cases, prefer the tokio
runtime - see The State of Async Rust: Runtimes⮳.
Alternatives to the Tokio async ecosystem include:
- ⮳: async version of the Rust standard library. No longer maintained?
- Smol⮳
- Embassy⮳ for embedded systems.
- Mio⮳ is a fast, low-level I/O library for Rust focusing on non-blocking APIs and event notification for building high performance I/O apps with as little overhead as possible over the OS abstractions. It is part of the Tokio ecosystem.
See also
Asynchronous Programming in Rust (book)⮳