Readers like you help support MUO. When you make a purchase using links on our site, we may earn an affiliate commission. Read More.

Traditional synchronous programming models often lead to performance bottlenecks. This is because the program waits for slow operations to complete before moving on to the next task. This often results in poor resource use and a sluggish user experience.

Asynchronous programming allows you to write non-blocking code that utilizes system resources effectively. By leveraging asynchronous programming, you can design apps that perform multiple tasks. Asynchronous programming is handy for handling several network requests or processing large amounts of data without blocking the execution flow.

Asynchronous Programming in Rust

Rust’s asynchronous programming model allows you to write efficient Rust code that runs concurrently without blocking execution flow. Asynchronous programming is beneficial when dealing with I/O operations, network requests, and tasks that involve waiting for external resources.

You can implement asynchronous programming in your Rust apps in several ways. These include language features, libraries, and the Tokio runtime.

Also, Rust's ownership model and concurrency primitives like channels and locks enable safe and efficient concurrent programming. You can leverage these features with asynchronous programming to build concurrent systems that scale well and utilize multiple CPU cores.

Rust's Asynchronous Programming Concepts

Futures provide a foundation for asynchronous programming in Rust. A future represents an asynchronous computation that hasn’t been completely executed.

Futures are lazy (they only get executed when on polling). When you call a future poll() method, it checks whether the future has been completed or needs additional work. If the future is not ready, it returns Poll::Pending, indicating that the task should be scheduled for later execution. If the future is ready, it returns Poll::Ready with the resulting value.

Rust's standard toolchain includes asynchronous I/O primitives, an async version of file I/O, networking, and timers. These primitives allow you to perform I/O operations asynchronously. This helps avoid blocking a program's execution while waiting for I/O tasks to complete.

The async/await syntax allows you to write asynchronous code that looks similar to synchronous code. This makes your code intuitive and easy to maintain.

open padlock with key beside it

Rust's approach to asynchronous programming emphasizes safety and performance. The ownership and borrowing rules ensure memory safety and prevent common concurrency issues. Async/await syntax and futures provide an intuitive way to express asynchronous workflows. You can use a third-party runtime to manage tasks for efficient execution.

You can combine these language features, libraries, and runtime to write highly-performant code. It provides a powerful and ergonomic framework for building asynchronous systems. This makes Rust a popular choice for projects requiring efficient handling of I/O-bound tasks and high concurrency.

Rust version 1.39 and later releases don't support asynchronous operations in Rust’s standard library. You’ll need a third-party crate to use the async/await syntax for handling asynchronous operations in Rust. You can use third-party packages like Tokio or async-std to work with the async/await syntax.

Asynchronous Programming With Tokio

Tokio is a robust asynchronous runtime for Rust. It provides functionality for building highly-performant and scalable applications. You can harness the power of asynchronous programming with Tokio. It also provides features for extensibility.

At the core of Tokio is its asynchronous task scheduling and execution model. Tokio allows you to write asynchronous code with the async/await syntax. This enables efficient system resource utilization and concurrent task execution. Tokio's event loop efficiently manages task scheduling. This ensures optimal utilization of CPU cores and minimizes context-switching overhead.

Tokio’s combinators make Task coordination and composition easy. Tokio provides powerful task coordination and composition tools. You can wait for multiple tasks to complete with join, select the first completed task with select and race tasks against each other with race.

Add the tokio crate to your Cargo.toml file's dependencies section.

 [dependencies]
tokio = { version = "1.9", features = ["full"] }

Here’s how you can use the async/await syntax in your Rust programs with Tokio:

 use tokio::time::sleep;
use std::time::Duration;

async fn hello_world() {
    println!("Hello, ");
    sleep(Duration::from_secs(1)).await;
    println!("World!");
}

#[tokio::main]
async fn main() {
    hello_world().await;
}

The hello_world function is asynchronous, so it can use the await keyword to pause its execution until a future is resolved. The hello_world function prints "Hello, " to the console. The Duration::from_secs(1) function call suspends the function execution for a second. The await keyword waits for the sleep future to complete. Finally, the hello_world function prints "World!" to the console.

result from running the hello_world function

The main function is an asynchronous function with the #[tokio::main] attribute. It designates the main function as the entry point for the Tokio runtime. The hello_world().await executes the hello_world function asynchronously.

Delaying Tasks With Tokio

A prevalent task in asynchronous programming is using delays or scheduling tasks to run in a specified time range. The tokio runtime provides a mechanism for using asynchronous timers and delays through the tokio::time module.

Here’s how you can delay an operation with the Tokio runtime:

 use std::time::Duration;
use tokio::time::sleep;

async fn delayed_operation() {
    println!("Performing delayed operation...");
    sleep(Duration::from_secs(2)).await;
    println!("Delayed operation completed.");
}

#[tokio::main]
async fn main() {
    println!("Starting...");
    delayed_operation().await;
    println!("Finished.");
}

The delayed_operation function introduces a delay of two seconds with the sleep method. The delayed_operation function is asynchronous, so it can use await to pause its execution until the delay is complete.

result from running the delayed operation function

Error Handling in Asynchronous Programs

Error handling in asynchronous Rust code involves using the Result type and handling Rust errors with the ? operator.

 use tokio::fs::File;
use tokio::io;
use tokio::io::{AsyncReadExt};

async fn read_file_contents() -> io::Result<String> {
    let mut file = File::open("file.txt").await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    Ok(contents)
}

async fn process_file() -> io::Result<()> {
    let contents = read_file_contents().await?;
    // Process the file contents
    Ok(())
}

#[tokio::main]
async fn main() {
    match process_file().await {
        Ok(()) => println!("File processed successfully."),
        Err(err) => eprintln!("Error processing file: {}", err),
    }
}

The read_file_contents function returns an io::Result that represents the possibility of an I/O error. By using the ? operator after each asynchronous operation, the Tokio runtime will propagate errors up the call stack.

The main function handles the result with a match statement that prints a text based on the outcome of the operation.

Reqwest Uses Asynchronous Programming for HTTP Operations

Many popular crates, including Reqwest, use Tokio to provide asynchronous HTTP operations.

You can use Tokio with Reqwest to make several HTTP requests without blocking other tasks. Tokio can help you handle thousands of concurrent connections and efficiently manage resources.