Enforcing the happens-before relationship by the join method so that the parent thread observes the changes made by the child thread

Here is the Rust code snippet:

use std::thread;
use std::sync::atomic::{AtomicBool, AtomicI32};
use std::sync::atomic::Ordering::Relaxed;

static DATA: AtomicI32 = AtomicI32::new(0);
static READY: AtomicBool = AtomicBool::new(false);

fn main()
{
    //child thread
    let t = thread::spawn(||
    {
        DATA.store(123, Relaxed);
        READY.store(true, Relaxed);
    });
    
    // parent thread
    t.join().expect("Thread is panicked!");

    while !READY.load(Relaxed) 
    {
        println!("waiting..");
    }
    println!("{}", DATA.load(Relaxed));
}

The code snippet uses only Relaxed ordering which does not enforce the happens-before relationship among or between threads. However, the join() method guarantees a happens-before relationship between what happened in the spawned/child thread and what happens after the join() call. Here the child thread is the modifying-thread and the parent thread is only accessing the data, so the parent thread observing the changes made by the child thread is paramount.

Do you think the join() method gives the guarantee to the main thread that it observes all the changes that happened in the child/spawned thread after the join() method call?

I am asking here because I am confused that Relaxed ordering might not force the changes that happened to DATA and READY in the child thread to be observed immediately and in no delayed manner(before the parent thread finishes its execution) to be observed in the main/parent thread.

The fact that it does not guarantee the order in itself does not mean that it actively prevents others from adding more synchronization.

A raw, non-atomic integer doesn't have any notion of ordering or synchronization (across threads), yet you can naïvely make it thread-safe by wrapping it in a mutex, for example.

Joining does synchronize, and it does ensure that the joined thread has already executed. I can't imagine any way it would be valid for the parent thread to not observe changes from the child thread after joining it.

2 Likes

join is enough. An easy way to understand this is that using thread::scope with non-atomic types also works.

use std::thread;

fn main() {
    let mut data = 0;
    let mut ready = false;

    thread::scope(|scope| {
        //child thread
        let t = scope.spawn(|| {
            data = 123;
            ready = true;
        });
        // join happens at the end of the scope
    });

    while !ready {
        println!("waiting.."); // never happens
    }
    println!("{}", data);
}
3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.