Skip to content
All essays
CraftMarch 15, 202414 min

Async Rust Programming

Master asynchronous programming in Rust with async/await

Ü
Ümit Uz
Mobile & Full Stack Developer

Asynchronous programming in Rust enables efficient concurrent operations without multiple threads. The async/await syntax makes writing asynchronous code as easy as synchronous code, while maintaining Rust's safety guarantees.

Understanding Async Rust

Async Rust provides a way to run multiple tasks concurrently on a single thread, avoiding the overhead of thread switching while still achieving high concurrency.

The Async Model

Rust's async model is based on futures:

rust
async fn hello_world() {
    println!("Hello, world!");
}

Async functions return futures that implement the Future trait.

Why Async?

Async is useful for I/O-bound operations:

rust
async fn fetch_data(url: &str) -> Result<String, Error> {
    let response = reqwest::get(url).await?;
    let text = response.text().await?;
    Ok(text)
}

This allows other tasks to run while waiting for I/O.

The Future Trait

All async operations are built on the Future trait.

Basic Definition

rust
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

Futures represent values that may not be available yet.

Poll Method

The poll method checks if the value is ready:

rust
enum Poll<T> {
    Ready(T),
    Pending,
}

Async/Await Syntax

The async/await syntax makes working with futures ergonomic.

Async Functions

rust
async fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

Async functions return a Future, not the direct value.

Await Operator

The .await operator waits for a future to complete:

rust
async fn example() {
    let greeting = greet("Alice").await;
    println!("{}", greeting);
}

.yield allows other tasks to run while waiting.

Runtimes

Async Rust needs a runtime to execute futures.

Tokio

Tokio is the most popular async runtime:

rust
use tokio;

#[tokio::main]
async fn main() {
    println!("Hello from Tokio!");
}

Tokio provides executors, I/O, timers, and more.

Async-std

Async-std is an alternative runtime:

rust
use async_std;

#[async_std::main]
async fn main() {
    println!("Hello from async-std!");
}

Executors

Executors poll futures to completion:

rust
use futures::executor::block_on;

fn main() {
    let future = async {
        println!("Hello!");
    };
    block_on(future);
}

Async I/O

Async I/O is the most common use case.

Reading Files

rust
use tokio::fs;

async fn read_file(path: &str) -> Result<String, io::Error> {
    let contents = fs::read_to_string(path).await?;
    Ok(contents)
}

Network Requests

rust
use reqwest;

async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?;
    let text = response.text().await?;
    Ok(text)
}

TCP Connections

rust
use tokio::net::TcpListener;

async fn listen() -> Result<(), io::Error> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    loop {
        let (socket, _) = listener.accept().await?;
        handle_connection(socket).await;
    }
}

Spawning Tasks

Tasks are independent async units.

Basic Spawning

rust
#[tokio::main]
async fn main() {
    tokio::spawn(async {
        println!("Hello from a task!");
    });
}

Tasks run concurrently on the runtime.

Waiting for Tasks

rust
use tokio::task::JoinHandle;

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        42
    });
    let result = handle.await.unwrap();
    println!("Result: {}", result);
}

Task Handles

JoinHandles allow you to wait for task completion:

rust
async fn spawn_and_wait() {
    let handle = tokio::spawn(async {
        // Do work
        123
    });
    let result = handle.await.unwrap();
}

Concurrency Patterns

Async enables powerful concurrency patterns.

Parallel Execution

rust
use tokio::join;

async fn fetch_both() {
    let (a, b) = tokio::join!(
        fetch_data("url1"),
        fetch_data("url2")
    );
}

Both requests run concurrently.

Select Macro

Wait for multiple operations:

rust
use tokio::select;

async fn example() {
    tokio::select! {
        _ = tokio::time::sleep(Duration::from_secs(1)) => {
            println!("Timeout!");
        }
        result = some_async_operation() => {
            println!("Got result: {:?}", result);
        }
    }
}

Racing Operations

rust
use tokio::select;

async fn race() {
    tokio::select! {
        Some(msg) = rx.recv() => {
            println!("Received: {}", msg);
        }
        _ = tokio::signal::ctrl_c() => {
            println!("Ctrl-C pressed");
        }
    }
}

Streams

Streams are async iterators.

Basic Usage

rust
use futures::stream::{Stream, StreamExt};

async fn process_stream(mut stream: impl Stream<Item = i32>) {
    while let Some(value) = stream.next().await {
        println!("Got: {}", value);
    }
}

Creating Streams

rust
use futures::stream;

async fn example() {
    let stream = stream::iter(vec![1, 2, 3, 4, 5]);
    stream.for_each(|value| async move {
        println!("{}", value);
    }).await;
}

Stream Combinators

rust
use futures::stream::StreamExt;

async fn process() {
    let mut stream = // some stream
    let doubled = stream.map(|x| x * 2);
    doubled.for_each(|value| async move {
        println!("{}", value);
    }).await;
}

Channels

Channels allow communication between tasks.

MPSC Channels

rust
use tokio::sync::mpsc;

async fn mpsc_example() {
    let (tx, mut rx) = mpsc::channel(100);

    tokio::spawn(async move {
        tx.send("Hello").await.unwrap();
    });

    let msg = rx.recv().await.unwrap();
    println!("Received: {}", msg);
}

Broadcast Channels

rust
use tokio::sync::broadcast;

async fn broadcast_example() {
    let (tx, mut rx1) = broadcast::channel(16);
    let mut rx2 = tx.subscribe();

    tokio::spawn(async move {
        tx.send("Hello").await.unwrap();
    });

    let msg1 = rx1.recv().await.unwrap();
    let msg2 = rx2.recv().await.unwrap();
}

Watch Channels

rust
use tokio::sync::watch;

async fn watch_example() {
    let (tx, mut rx) = watch::channel(0);

    tokio::spawn(async move {
        tx.send(1).unwrap();
    });

    while rx.changed().await.is_ok() {
        println!("New value: {}", *rx.borrow());
    }
}

Timeouts and Delays

Async provides time-based operations.

Sleep

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

async fn delay_example() {
    sleep(Duration::from_secs(1)).await;
    println!("Waited 1 second");
}

Timeout

rust
use tokio::time::timeout;

async fn timeout_example() -> Result<(), Error> {
    timeout(Duration::from_secs(1), long_operation()).await?
}

Interval

rust
use tokio::time::interval;

async fn interval_example() {
    let mut interval = interval(Duration::from_secs(1));
    loop {
        interval.tick().await;
        println!("Tick!");
    }
}

Error Handling

Async error handling uses Result.

Propagating Errors

rust
async fn fetch() -> Result<String, Error> {
    let response = reqwest::get(url).await?;
    let text = response.text().await?;
    Ok(text)
}

Handling Errors

rust
async fn handle_errors() {
    match fetch().await {
        Ok(text) => println!("Success: {}", text),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Cancellation

Async tasks can be cancelled.

Drop Handles

rust
async fn cancellation_example() {
    let handle = tokio::spawn(async {
        // Long-running task
    });
    handle.abort(); // Cancel the task
}

Cooperative Cancellation

rust
use tokio::select;

async fn cooperative() {
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            println!("Cancelled");
        }
        result = work() => {
            println!("Completed: {:?}", result);
        }
    }
}

Best Practices

  1. 1Use async for I/O-bound operations
  2. 2Keep async tasks small and focused
  3. 3Use structured concurrency patterns
  4. 4Handle errors properly
  5. 5Be aware of cancellation
  6. 6Use appropriate channels for communication
  7. 7Profile performance before optimizing

Common Pitfalls

Blocking in Async

rust
// Don't do this:
async fn bad() {
    std::thread::sleep(Duration::from_secs(1)); // Blocks!
}

// Do this instead:
async fn good() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

Forgetting Await

rust
// Don't do this:
async fn bad() {
    fetch_data(url); // Future is dropped!
}

// Do this instead:
async fn good() {
    fetch_data(url).await;
}

Holding Across Await

rust
// Don't do this:
async fn bad(mutex: &Mutex<i32>) {
    let guard = mutex.lock().unwrap();
    some_async_operation().await;
    // Lock is held during await!
}

// Do this instead:
async fn good(mutex: &Mutex<i32>) {
    let value = {
        let guard = mutex.lock().unwrap();
        *guard
    };
    some_async_operation().await;
}

Performance Considerations

Async reduces overhead for I/O-bound tasks:

rust
// Can handle thousands of concurrent connections
async fn handle_connections() {
    let listener = TcpListener::bind("0.0.0.0:8080").await?;
    loop {
        let (socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            handle_connection(socket).await;
        });
    }
}

Conclusion

Async Rust enables efficient concurrent programming with the safety guarantees Rust is known for. The async/await syntax makes writing asynchronous code as easy as synchronous code, while runtimes like Tokio provide the necessary infrastructure.

Master async Rust, and you'll build applications that handle thousands of concurrent operations with minimal overhead. The combination of async/await with Rust's ownership model creates a powerful paradigm for building scalable systems.

Practice with async patterns, understand the runtime model, and leverage the ecosystem. Async Rust is the key to building high-performance, concurrent applications without sacrificing safety.

Enjoyed this essay? Share it.

Related essays

Next essay
Rust Lifetimes Deep Dive