As a Rust systems programmer, you may often run into concepts like async, .await, and tokio::runtime::block_on(). These are essential tools for writing efficient, non-blocking applications—but they're also often misunderstood by beginners.

In this blog post, we will break down:

  • What async and .await do in Rust
  • Why block_on() exists and when to use it
  • How these differ from traditional synchronous (blocking) functions
  • Real-world examples with code
  • A comparison of blocking vs async behavior
  • Multi-threading vs async in practical Rust usage

Why Use async?

Rust's async support enables you to write code that can perform long-running operations—like file I/O or HTTP requests—without blocking the entire thread.

async fn say_hello() {
    println!("Hello from async!");
}

To actually run this, you need to use .await inside an async context or call it using block_on() if you're in sync code:

use tokio::runtime::Runtime;

fn main() {
    let rt = Runtime::new().unwrap();
    rt.block_on(say_hello());
}

What Does block_on() Do?

block_on() is a method that runs an async function from a synchronous context, like main(). It will:

  1. Start the async task
  2. Block the thread until the task completes

This is especially useful in CLI applications or testing where you don't want your whole program to be async.


Comparing Sync vs Async

🔴 Blocking (Synchronous)

fn fetch_data() -> String {
    std::thread::sleep(std::time::Duration::from_secs(2));
    "Result".to_string()
}

fn main() {
    let result = fetch_data();
    println!("{}", result); // Takes ~2 seconds
}

✅ Async (Non-Blocking)

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

async fn fetch_data() -> String {
    sleep(Duration::from_secs(2)).await;
    "Result".to_string()
}

#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("{}", result); // Also takes ~2 seconds
}

So what’s the benefit?


✨ Async Shines with Concurrency

Let’s see the difference when running two tasks.

🔴 Synchronous

fn task1() -> String {
    std::thread::sleep(std::time::Duration::from_secs(2));
    "Task 1".to_string()
}

fn task2() -> String {
    std::thread::sleep(std::time::Duration::from_secs(2));
    "Task 2".to_string()
}

fn main() {
    let start = std::time::Instant::now();
    println!("{} and {}", task1(), task2());
    println!("Elapsed: {:?}", start.elapsed()); // ~4s
}

✅ Asynchronous

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

async fn task1() -> String {
    sleep(Duration::from_secs(2)).await;
    "Task 1".to_string()
}

async fn task2() -> String {
    sleep(Duration::from_secs(2)).await;
    "Task 2".to_string()
}

#[tokio::main]
async fn main() {
    let start = std::time::Instant::now();
    let (a, b) = tokio::join!(task1(), task2());
    println!("{} and {}", a, b);
    println!("Elapsed: {:?}", start.elapsed()); // ~2s
}

With tokio::join!, async functions run concurrently, reducing total time dramatically.


🧵 Real-World Multi-threaded Examples

Besides async, Rust also supports multi-threading, which is ideal for CPU-bound tasks.

🧶 Using std::thread for Concurrency

use std::thread;

fn main() {
    let handle1 = thread::spawn(|| {
        println!("Thread 1 doing work");
    });

    let handle2 = thread::spawn(|| {
        println!("Thread 2 doing work");
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

This is best for:

  • Parallel file processing
  • Heavy computation (hashing, compression, etc.)
  • Running background workers

📌 Comparison Table

Task Type Use Async? Use Threads? Example
HTTP API call ✅ Yes 🚫 No Wallet querying UTXOs
File reading (large) ✅ Often ✅ Sometimes Loading blockchain DB
Cryptographic ops 🚫 No ✅ Yes Signing transactions
Event listeners ✅ Yes 🚫 No Listening to new blocks

🧠 When to Use What?

Goal Use
Simple script or one-off call block_on()
Multi-request wallet scanner .await with join!() or spawn()
CPU-bound hashing jobs std::thread or rayon
HTTP/web server or bot Fully async main()

Final Thoughts

Using async in Rust lets you handle more work with fewer threads. block_on() is a bridge for using async in sync environments like CLI tools. Meanwhile, multithreading is still powerful and sometimes more appropriate for CPU-intensive or truly parallel tasks.

If you’re building something like a wallet app that fetches UTXOs or broadcasts transactions, using async functions for HTTP and network requests is a must for efficiency. But for cryptographic signing or background indexing, multi-threading might be better.

Understanding this tradeoff gives you the power to write high-performance, scalable Rust applications.