Skip to Content
Course content

119: Barrier for Thread Synchronization

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Barrier configured for 4 threads.
  • Spawn 4 threads. Each thread should:
    1. Print that it is "Generating noise for chunk X...".
    2. Sleep for a random duration between 10ms and 100ms to simulate work.
    3. Call wait() on the barrier.
    4. 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.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.