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:
- Start the async task
- 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.