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
154: Spawning Tasks with tokio::spawn
Why can't I just .await my futures? When do I actually need spawn?
This is where a lot of people get tripped up. When you call .await on a future, you're telling the current task to pause until that specific future completes. It's sequential. If you have three API calls and you .await each one in order, you're just doing synchronous programming with extra steps. You aren't actually doing anything in parallel.
tokio::spawn is different. It takes a future and hands it over to the Tokio runtime to be executed independently. The runtime can then move that task to a different thread if it wants to. It's the difference between waiting for the toaster to finish before you start the coffee, and starting both at the same time.
use tokio::time::{sleep, Duration};
async fn fetch_data(id: u32) {
println!("Starting fetch {}", id);
sleep(Duration::from_secs(2)).await;
println!("Finished fetch {}", id);
}
#[tokio::main]
async fn main() {
// This is slow. It takes 4 seconds total.
fetch_data(1).await;
fetch_data(2).await;
// This is fast. It takes 2 seconds total.
tokio::spawn(fetch_data(1));
tokio::spawn(fetch_data(2));
// We sleep here just so the main function doesn't exit
// before the spawned tasks finish.
sleep(Duration::from_secs(3)).await;
}
Why does the compiler keep complaining about 'static lifetimes?
I've spent way too many hours fighting this error. You'll likely see a message saying the future may outlive the current function, and that it requires a 'static lifetime.
Here's the deal: since tokio::spawn pushes the task onto the runtime's scheduler, Tokio has no idea when that task will actually finish. It might finish in a millisecond, or it might run for an hour. Because of that, the task cannot hold any references to data on the local stack—because that stack frame will be gone long before the task finishes. Everything the task uses must be owned by the task itself.
The fix is usually the move keyword. By putting move before the async block, you tell Rust to transfer ownership of the variables into the task.
use std::sync::Arc;
async fn process_config(config: Arc<String>) {
println!("Processing: {}", config);
}
#[tokio::main]
async fn main() {
let config = Arc::new("ServerAddr=127.0.0.1".to_string());
// This would fail without 'move' because 'config' is a local variable
tokio::spawn(async move {
process_config(config.clone()).await;
});
}
How do I get a value back from a spawned task?
When you call tokio::spawn, it doesn't just return "nothing." It returns a JoinHandle. Think of this as a receipt for the work you just ordered. You can .await that handle later to retrieve the return value of the async block.
One thing to watch out for: awaiting a JoinHandle returns a Result. This is because the task might have panicked. You have to handle that possibility, even if you're sure your code is safe.
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
// Simulate some heavy calculation
42
});
// Do other work here...
match handle.await {
Ok(value) => println!("The answer is {}", value),
Err(e) => eprintln!("The task panicked: {:?}", e),
}
}
📋 Practical Task
Exercise: Building a Concurrent Website Status Checker
Your goal is to create a program that checks the availability of multiple URLs concurrently. Instead of checking them one by one, you will spawn a separate Tokio task for each URL.
- Create a list of at least three URLs (you can use
"https://google.com", etc.). - Use a loop to iterate through the URLs.
- Inside the loop, use
tokio::spawnto start a task that simulates a network request (usetokio::time::sleepfor 1-2 seconds to mimic latency). - Each spawned task should return a string:
"URL [address] is UP". - Collect all the
JoinHandles into aVec. - After the loop, iterate through your
Vecof handles and.awaitthem to print the final status of every site.
There are no comments for now.