Introduction to Asynchronous Programming in Rust
Asynchronous programming is a paradigm that allows performing I/O operations (network requests, file reading, database operations) without blocking the main execution thread. In Rust, asynchrony is implemented through the Future system and the async/await keywords. Unlike multithreading, asynchronous code runs in a single thread, switching between tasks while waiting.
In this guide, we will cover:
- Basics of
async/awaitin Rust - Working with the
tokiocrate — the most popular async runtime - Creating and combining
Future - Practical examples: async HTTP client and timers
This article is aimed at developers familiar with basic Rust syntax. If you are just starting out, we recommend first studying the basics of the language on PythonLib.
Basics of async/await and Future in Rust
What is a Future?
Future is a trait representing an asynchronous operation that may complete in the future. In Rust, Future is lazy: it does nothing until it is polled. The async runtime (e.g., Tokio) is responsible for polling.
A simple example of an async function:
use std::time::Duration;
async fn say_hello() { println!("Hello from the async world!"); tokio::time::sleep(Duration::from_secs(1)).await; println!("1 second has passed");}
#[tokio::main]async fn main() { say_hello().await;}The async keyword turns a function into a Future. Calling .await suspends the execution of the current task until the Future completes, without blocking the thread.
Lifecycle of an async task
When you call .await, the following happens:
- The runtime polls the Future (calls the
poll()method) - If the Future is ready — the result is returned
- If the Future is waiting (e.g., a network operation) — the runtime switches to another task
- When the resource becomes available, the runtime resumes the task
An important feature of Rust: .await can only be called inside an async function or an async {} block. Attempting to use it in a synchronous context will result in a compilation error.
Practical Work with Tokio
Installation and Setup
Tokio is an async runtime for Rust, providing a task scheduler, timers, network sockets, and much more. To get started, add to Cargo.toml:
[dependencies]tokio = { version = "1", features = ["full"] }The features = ["full"] flag enables all Tokio features. For production, it is better to select only the necessary features.
Running Multiple Async Tasks
One of the main advantages of asynchrony is the parallel execution of many tasks. Use tokio::spawn to run tasks in the background:
use tokio::time::{sleep, Duration};
async fn task(name: &str, seconds: u64) { println!("Task {} started", name); sleep(Duration::from_secs(seconds)).await; println!("Task {} completed after {} sec", name, seconds);}
#[tokio::main]async fn main() { let handle1 = tokio::spawn(task("A", 2)); let handle2 = tokio::spawn(task("B", 1));
// Wait for both tasks to complete let _ = tokio::join!(handle1, handle2); println!("All tasks completed");}The tokio::join! macro waits for all passed Futures to complete. If one task finishes earlier, execution does not interrupt — we wait for the others.