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
45: The Tokio Runtime Basics
Up until now, we've talked about async and await as language features. But here is the secret that often trips people up when they first dive into Rust: the language itself doesn't actually know how to execute an async function. It just knows how to build a state machine that can be executed. To actually make something happen, you need a runtime. In the Rust ecosystem, that almost always means Tokio.
The Illusion of Concurrency
When I first started with Tokio, I made a classic mistake. I thought that because I was using async functions, I was automatically getting concurrency. I wrote a health checker that looped through a list of server IPs and used runtime.block_on() inside the loop. It looked like this:
// The naive, "fake async" way
for ip in server_list {
// This blocks the current thread until the future completes
let status = runtime.block_on(check_health(ip));
println!("Server {} is {}", ip, status);
}
I felt clever because I was using async functions, but in reality, I had just written a synchronous program with extra steps. block_on tells the runtime: "Stop everything else on this thread and do not move a single inch until this specific future is finished." If you have 100 servers and each takes one second to respond, your program takes 100 seconds. You've completely defeated the purpose of asynchronous I/O.
Letting the Scheduler Take the Wheel
The "correct" way to handle this is to stop trying to control the execution yourself and instead hand your tasks over to the Tokio scheduler. This is where #[tokio::main] and tokio::spawn come in. Instead of blocking the thread, we spawn the future onto the runtime. This tells Tokio: "Here is a piece of work. Run it whenever you have a free thread, and let me know when it's done."
#[tokio::main]
async fn main() {
let mut handles = vec![];
for ip in server_list {
// We don't await here! We spawn a background task.
let handle = tokio::spawn(async move {
check_health(ip).await
});
handles.push(handle);
}
// Now we wait for all the spawned tasks to finish
for handle in handles {
let _ = handle.await;
}
}
Now, the behavior changes entirely. Tokio's multi-threaded scheduler sees those 100 spawned tasks and distributes them across all available CPU cores. While one task is waiting for a network packet to arrive from Server A, the scheduler swaps it out and uses that thread to start the request for Server B. Your 100-second program suddenly takes as long as the slowest single server response.
The Hidden Cost of Spawning
You might wonder why we don't just spawn everything. There's a trade-off here. When you use tokio::spawn, the future you provide must be Send and have a 'static lifetime. This is because Tokio can move that task from one worker thread to another to balance the load (this is called work-stealing).
This is where you'll start hitting the "borrow checker wall." You can't easily pass a reference to a local variable into a spawned task because the runtime might outlive the function that created the variable. I've spent plenty of hours fighting std::sync::Arc and Mutex because of this. It's the price you pay for true, multi-threaded concurrency. If your task is very small and doesn't involve heavy I/O, the overhead of spawning and the complexity of managing Arc might actually make your code slower or more verbose than just using a simple join_all on a collection of futures.
📋 Practical Task
Exercise: Building a Concurrent Port Scanner
Your goal is to create a basic port scanner that checks whether a set of ports on localhost are open. Instead of checking them one-by-one (synchronously), you will use the Tokio runtime to check them concurrently.
Requirements:
- Create a list of ports to scan (e.g.,
vec![80, 443, 8080, 22, 3000]). - Use
#[tokio::main]to initialize the runtime. - Use
tokio::spawnto create a separate task for each port check. - Inside each task, attempt to connect to
"127.0.0.1:port"usingtokio::net::TcpStream::connect. - Print whether the port is "Open" or "Closed" based on whether the connection succeeded.
- Ensure the main function waits for all spawned tasks to complete before exiting.
There are no comments for now.