Search code examples
multithreadingrust

How to share a boolean flag with a scoped thread in Rust?


I am trying to use a boolean flag to signal that a thread should exit. However, currently my code violates the borrow checker rules. I understand why it violates these rules, however what I do not know is what the most appropriate design is and how that may fix the problem.

fn long_running(exit_flag: &bool) {

    loop {
        std::thread::sleep(std::time::Duration::from_secs(10));

        if *exit_flag {
            break;
        }
    }
}

fn main() {

    let mut exit_flag = false;
    std::thread::scope(
        |scope| {
            let handle = scope.spawn(
                || {
                    long_running(&exit_flag); # (1)
                }
            );

            exit_flag = true; # (2)

            // terminate the consumer poll loop
            handle.join().expect("failed to join thread");
        }
    );
}

The problem involves the lines marked with # (1) and # (2).

  • # (1): Reference to exit_flag is borrowed
  • # (2): Attempting to mutate underlying variable while reference is still borrowed

This is not permitted because of the borrow checker rules.

  • One solution to the problem might be to heap allocate the flag, and use this to share the data between the two threads.
  • However, the intention of using std::thread::scope was to permit launching a thread which has access to a local variable from the stack of the parent (main) thread.

btw got the idea of doing this from this thread


Solution

  • This does not work because exit_flag is to be borrowed by the thread's closure while also being mutated via exit_flag = true;. In other words, there is no guarantee that the thread has stopped observing exit_flag while it is being mutated (which in fact is the whole point of exit_flag).

    You'll need an AtomicBool to do that:

    use std::sync::atomic::{AtomicBool, Ordering};
    
    fn long_running(exit_flag: &AtomicBool) {
        loop {
            println!("Working...");
            std::thread::sleep(std::time::Duration::from_secs(3));
            if exit_flag.load(Ordering::Relaxed) {
                break;
            }
        }
    }
    
    fn main() {
        let exit_flag = AtomicBool::new(false);
        std::thread::scope(|scope| {
            scope.spawn(|| {
                long_running(&exit_flag);
            });
            exit_flag.store(true, Ordering::Relaxed);
        });
        println!("Done");
    }
    

    Notice that I removed handle.join(), as thread::scope already guarantees that all threads have been joined when it returns.