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
119: Barrier for Thread Synchronization
When you first start working with multi-threading in Rust, you'll likely rely heavily on join() to make sure your main thread doesn't exit before your workers finish. Because of this, a common misconception I see is the belief that join() is the primary tool for all thread synchronization. You might think, "If I need Thread B to wait for Thread A, I'll just join Thread A first."
The Trap: Thinking Join Handles Manage Mid-Process Sync
The problem with relying on join() is that it's a "one-and-done" operation. It waits for a thread to terminate. But in real-world software—like a game engine or a scientific simulation—you often have threads that need to stay alive for the duration of the program, but must synchronize at specific milestones.
Imagine you're building a renderer. You have three threads: one for geometry, one for lighting, and one for textures. You can't start the "Composition" phase until all three have finished their respective "Preparation" phases. If you used join(), you'd have to kill and respawn your threads for every single frame of the animation. That's an absurd amount of overhead and just plain wrong.
Coordinating Phase Shifts with std::sync::Barrier
This is where std::sync::Barrier comes in. Think of a barrier as a checkpoint. You tell the barrier how many threads are expected to arrive, and any thread that hits the wait() method will block right there until the last expected thread arrives. Once the threshold is hit, the "gate" opens, and everyone is released simultaneously to proceed to the next phase.
Here is how I would implement that asset loading scenario. Notice how we wrap the barrier in an Arc so every thread can share ownership of the same checkpoint.
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
fn main() {
// We have 3 worker threads and 1 main thread that we want to sync
// Let's say we only care about syncing the 3 workers.
let barrier = Arc::new(Barrier::new(3));
let mut handles = Vec::new();
for i in 0..3 {
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
println!("Thread {}: Loading assets...", i);
// Simulate varying workloads
thread::sleep(Duration::from_millis(100 * i as u64));
println!("Thread {}: Reached checkpoint, waiting for others...", i);
// This is where the magic happens.
// The thread stops here until 3 calls to wait() have occurred.
let wait_result = b.wait();
// One (and only one) thread is designated as the leader.
// This is useful for performing a single cleanup task before the next phase.
if wait_result.is_leader() {
println!("--- All assets loaded. Leader thread is initializing the scene ---");
}
println!("Thread {}: Now starting the rendering phase!", i);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
A quick tip: pay attention to that is_leader() method on the BarrierWaitResult. I've found it incredibly useful for logging or triggering a single "global" event (like updating a shared state) that only needs to happen once per phase, without needing to create yet another Mutex or atomic flag.
Keep in mind that barriers can easily lead to deadlocks if you aren't careful. If you initialize a Barrier::new(3) but only spawn two threads that call wait(), your program will hang forever. The barrier is a strict contract: it will not let anyone pass until the count is exactly met.
📋 Practical Task
Exercise: Parallel Map Chunk Generator
You are building a world generator that creates a map in "chunks." To ensure the borders between chunks are seamless, all threads must finish generating their raw noise data before any thread is allowed to run the "Smoothing Pass" (which reads data from neighboring chunks).
Requirements:
- Create a
Barrierconfigured for 4 threads. - Spawn 4 threads. Each thread should:
- Print that it is "Generating noise for chunk X...".
- Sleep for a random duration between 10ms and 100ms to simulate work.
- Call
wait()on the barrier. - Print that it is now "Smoothing borders for chunk X...".
- Ensure that no "Smoothing" messages appear in the console until all "Generating" messages have been printed.
- Use the
is_leader()method to print "--- All chunks generated. Starting global smoothing pass ---" exactly once.
There are no comments for now.