Asynchronous Programming in Rust: A Complete Practical Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction to Asynchronous Programming in Rust



Modern applications increasingly require high performance when working with networks, file systems, or databases. Asynchronous programming in Rust allows you to efficiently handle thousands of concurrent connections without creating a thread for each task. Unlike multithreading, asynchronous code uses less memory and scales better.



Rust offers a unique approach to asynchrony based on zero-cost abstractions. This means you only pay for what you use — no overhead for unused features. In this guide, we will break down key concepts and write practical examples that will help you start writing asynchronous code in Rust today.



Basics of async/await and Futures



What is a Future?



Future is a trait representing an asynchronous operation that may complete in the future. In Rust, Future is a lazy entity: it does nothing until it is polled. The main idea: instead of blocking a thread, we register a task and get the result when it is ready.



use std::future::Future;use std::pin::Pin;use std::task::{Context, Poll};

struct MyFuture { ready: bool,}

impl Future for MyFuture { type Output = u32;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> { if self.ready { Poll::Ready(42) } else { // Register waker for re-polling cx.waker().wake_by_ref(); Poll::Pending } }}


async/await — Syntactic Sugar



The async keyword transforms a function or block into a Future. await suspends execution until the Future completes, returning control to the runtime.



use tokio::time::{sleep, Duration};

async fn fetch_data() -> String { // Simulating a network request sleep(Duration::from_secs(2)).await; String::from("Data received")}

#[tokio::main]async fn main() { let result = fetch_data().await; println!("{}", result);}


Lifetimes and the Borrow Checker



One of Rust's features is strict borrow checking. In asynchronous code, this is especially important because a Future may outlive borrowed data. Use async move to capture ownership:



async fn process_data(data: Vec<u32>) -> u32 {    data.iter().sum()}

#[tokio::main]async fn main() { let numbers = vec![1, 2, 3, 4, 5]; let sum = process_data(numbers).await; println!("Sum: {}", sum);}


Practical Work with Tokio



Installation and Setup



Tokio is the most popular asynchronous runtime for Rust. Add to Cargo.toml:



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


Creating an Asynchronous TCP Server



Let's look at a simple echo server that accepts connections and sends back the received data:



use tokio::net::TcpListener;use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let listener = TcpListener::bind("127.0.0.1:8080").await?; println!("Server started on port 8080");

loop { let (mut socket, addr) = listener.accept().await?; println!("New connection from: {}", addr);

tokio::spawn(async move { let mut buf = [0; 1024]; loop { let n = socket.read(&mut buf).await.unwrap(); if n == 0 { break; } // Connection closed socket.write_all(&buf[..n]).await.unwra

Blogs

Book Recommendations