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
149: Select! Macro for Racing Futures
When you first encounter the select! macro, it's easy to assume it behaves like a switch statement for asynchronous events. You likely think: "I'll just put my futures inside the select! block, and whenever one finishes, the loop will restart and wait for them all again."
The Myth of the Automatic Pause
Here is the trap: if you define your futures inside the select! macro within a loop, you aren't pausing them. You are destroying them. Every time a different branch of the select! wins, the other futures are dropped. When the loop iterates and hits the select! again, those futures are recreated from scratch.
I've seen this bite a lot of engineers building network proxies. Imagine you're waiting for a TCP stream to provide data, but you also have a timeout timer running. If the timer wins, the loop restarts. If you created the stream.read() future inside that block, you just dropped the future that was potentially halfway through reading a packet. You've effectively reset your progress on that I/O operation.
// DON'T DO THIS in a loop loop { tokio::select! { res = socket.read(&mut buf) => { // Handle data } _ = tokio::time::sleep(Duration::from_secs(5)) => { println!("Timeout! But I just dropped the read future!"); // The next iteration creates a BRAND NEW read future. } } }Preserving State with Pinning and Fusing
To actually "race" futures without losing your place, you have to move the futures outside the loop. But there's a catch:
select!requires futures to beUnpin(because it needs to poll them multiple times) andFusedFuture(so it knows when a future is completely finished and should no longer be polled).The standard pattern I use is to create the futures first,
pin_mut!them to the stack, and usefuse(). This tells Rust, "This future stays right here in memory, and if it returnsPoll::Readyonce, it's done for good."Let's look at a real-world scenario: a request that needs to stay alive as long as a "heartbeat" signal is arriving, but should time out if the heartbeat stops.
use tokio::time::{sleep, Duration}; use futures::future::{FutureExt, fuse}; use tokio::pin; async fn race_with_heartbeat() { let request_fut = async { sleep(Duration::from_secs(10)).await; "Request Finished!" }.fuse(); let heartbeat_fut = async { sleep(Duration::from_secs(2)).await; "Heartbeat received" }.fuse(); pin!(request_fut); pin!(heartbeat_fut); let mut heartbeat_count = 0; loop { tokio::select! { res = &mut request_fut => { println!("Success: {}", res); break; } _ = &mut heartbeat_fut => { heartbeat_count += 1; println!("Heartbeat {} received, still waiting for request...", heartbeat_count); // We need to "reset" the heartbeat timer for the next race heartbeat_fut.set(async { sleep(Duration::from_secs(2)).await; "Heartbeat received" }.fuse()); } } } }Notice the
&mut. We aren't giving the future to the macro; we're giving it a mutable reference. This allows the loop to continue polling the samerequest_futacross multiple iterations. I personally find that usingpin!is much cleaner thanBox::pinunless you actually need to move the future into another thread, which we aren't doing here.
📋 Practical Task
Build a Concurrent Shutdown Signal Handler
You are building a background worker that processes a stream of simulated jobs. However, the worker must be able to shut down immediately if a "shutdown" signal is received, regardless of whether a job is currently processing.
Requirements:
- Create a
process_jobfuture that takes 3 seconds to complete. - Create a
shutdown_signalfuture that resolves after 5 seconds (simulating a SIGINT). - Use a
loopandtokio::select!to process jobs one by one. - Ensure that the
process_jobfuture is pinned and fused outside the loop so that if you were to add a secondary timer, you wouldn't lose progress. - If the
shutdown_signalwins, print "Shutdown received, exiting..." and break the loop. - If a job finishes, print "Job completed!", and then replace the
process_jobfuture with a new one to start the next job.
There are no comments for now.