Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
44: Async Rust with async/await
I've seen a lot of developers move into Rust and treat asynchronous programming as if it were just "multithreading with less boilerplate." Itβs a common trap. In reality, async in Rust is a completely different mental model. When you mark a function as async, you aren't telling the compiler to run it on another thread; you're telling it to transform that function into a state machine that can be paused and resumed.
The Cost of Waiting in Line
Let's look at a scenario I deal with often: fetching metadata for a list of remote resources. Imagine you have a list of ten different API endpoints, and you need to get a status report from each. The naive way to do this is a simple for loop with a blocking HTTP client.
// The naive, blocking approach
fn fetch_all_statuses(urls: Vec&str]) -> Vec<String> {
let mut results = Vec::new();
for url in urls {
// This blocks the entire thread until the server responds
let response = reqwest::blocking::get(url).unwrap().text().unwrap();
results.push(response);
}
results
}
This works, but it's agonizingly slow. If each request takes 200ms, you're spending two full seconds just sitting there. Your CPU is doing absolutely nothing while the network card waits for packets to fly across the ocean. You might think, "I'll just wrap each call in std::thread::spawn," but threads are expensive. Spawning a thousand threads for a thousand requests will eat your RAM and spend more time context-switching than actually doing work.
Stop Waiting, Start Scheduling
This is where async/await comes in. When we switch to an async runtime like tokio, we stop thinking about "blocking" and start thinking about "yielding." In the async version, when you .await a request, you're essentially saying: "I can't go any further until this data arrives. Go ahead and use this thread to do something else in the meantime."
However, there's a subtle trap here. If you just put .await inside a loop, you're still running things sequentially. You're just doing it with more expensive syntax.
// Still sequential, just using async keywords
async fn fetch_all_statuses_naive(urls: Vec<String>) -> Vec<String> {
let mut results = Vec::new();
for url in urls {
// We are still waiting for one to finish before starting the next
let response = reqwest::get(url).await.unwrap().text().await.unwrap();
results.push(response);
}
results
}
To actually get the performance boost we're after, we need to create a collection of Futures and then drive them to completion concurrently. A Future in Rust is lazy; it does nothing until it is polled. By creating a list of futures and using something like join_all, we tell the runtime to track all of them at once.
use futures::future::join_all;
async fn fetch_all_statuses_efficient(urls: Vec<String>) -> Vec<String> {
let tasks = urls.into_iter().map(|url| async move {
reqwest::get(url).await.unwrap().text().await.unwrap()
});
// join_all polls all the futures concurrently on the runtime
join_all(tasks).await
}
The Hidden Trade-off: Complexity and Lifetimes
Now, I have to be honest with you: this isn't a free lunch. The moment you move into async Rust, you're going to fight the borrow checker more than usual. Because an async block can be paused and resumed, the compiler has to ensure that any reference you hold stays valid for the entire life of the Future.
You'll find yourself using Arc and Mutex more frequently, and you'll run into the dreaded 'static lifetime requirement when spawning tasks. If you spawn a task with tokio::spawn, the runtime has no idea how long that task will run, so it cannot hold references to local variables on the stack. You have to move ownership into the async block using the move keyword.
My rule of thumb? Don't go async unless you are I/O bound. If you're doing heavy math or image processing, async will actually slow you down due to the overhead of the state machine. But for network requests and database queries, it's the only way to build a system that scales.
π Practical Task
Exercise: Concurrent Website Health Checker
Build a small utility that takes a list of URLs and checks their HTTP status codes concurrently. Your program should not wait for one website to respond before requesting the next.
- Use the
tokioruntime and thereqwestcrate (with thejsonfeature). - Create a function
async fn check_health(url: String) -> Result<u16, reqwest::Error>that returns the status code of a given URL. - In your
mainfunction (marked with#[tokio::main]), initialize a vector of at least five different URLs. - Use
futures::future::join_allto execute these checks concurrently. - Print the final results in the format:
URL: [url] - Status: [code].
Challenge: Handle the errors gracefully. If a URL is invalid or the server is down, instead of calling .unwrap(), return a custom error message or a 0 status code so that one failing request doesn't crash the entire batch.
There are no comments for now.