Waitgroups in Rust

Our example demonstrates how to wait for multiple threads to finish using a WaitGroup. In Rust, we’ll use the std::sync::Arc and std::sync::Barrier to achieve similar functionality.

use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;

// This is the function we'll run in every thread.
fn worker(id: i32) {
    println!("Worker {} starting", id);

    // Sleep to simulate an expensive task.
    thread::sleep(Duration::from_secs(1));
    println!("Worker {} done", id);
}

fn main() {
    // This Barrier is used to wait for all the
    // threads launched here to finish.
    let barrier = Arc::new(Barrier::new(5));

    // Launch several threads and clone the Barrier
    // for each.
    let mut handles = vec![];
    for i in 1..=5 {
        let b = Arc::clone(&barrier);
        
        // Spawn a new thread and store its handle
        let handle = thread::spawn(move || {
            worker(i);
            // Wait for all threads to reach this point
            b.wait();
        });
        handles.push(handle);
    }

    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }

    // Note that this approach has no straightforward way
    // to propagate errors from workers. For more
    // advanced use cases, consider using channels or
    // other synchronization primitives.
}

To run the program:

$ cargo run
Worker 5 starting
Worker 1 starting
Worker 4 starting
Worker 3 starting
Worker 2 starting
Worker 4 done
Worker 1 done
Worker 2 done
Worker 5 done
Worker 3 done

The order of workers starting up and finishing is likely to be different for each invocation.

In this Rust version:

  1. We use Arc<Barrier> instead of WaitGroup. Arc provides thread-safe reference counting, allowing us to share the Barrier across multiple threads.

  2. The Barrier is created with a count of 5, matching the number of threads we’ll spawn.

  3. We spawn threads using thread::spawn and store their handles in a vector.

  4. Each thread calls worker() and then waits at the barrier using b.wait().

  5. In the main thread, we wait for all spawned threads to complete by calling join() on each handle.

This approach ensures all threads complete before the program exits, similar to the WaitGroup in the original example. However, Rust’s ownership and borrowing rules provide additional safety guarantees at compile time.