Skip to content

Atomic load makes threads not explore at all #392

@ZobyTwo

Description

@ZobyTwo

Loom uses a per-thread state to remember the status of threads at branching points.
When a newly spawned thread immediately does an atomic load, the code in Execution::schedule ends up setting that to Thread::Skip, disabling any exploration, which is clearly wrong.

This reproduces the case:

#[test]
fn thread_skipped() {
    let solutions = std::sync::Arc::new(std::sync::Mutex::new(HashSet::new()));
    let solutions1 = std::sync::Arc::clone(&solutions);

    loom::model(move || {
        let a1 = Arc::new(AtomicBool::new(false));
        let a2: Arc<AtomicBool> = a1.clone();

        let th = thread::spawn(move || {
            // Some irrelevant load, which causes loom to not explore properly.
            let _irrelevant = a1.load(Relaxed);
            a1.store(true, Relaxed);
        });

        let update_done = a2.load(Relaxed); // Could be either true or false

        th.join().unwrap();

        solutions1.lock().unwrap().insert(update_done);
    });

    let solutions = solutions.lock().unwrap();
    
    assert!(solutions.contains(&false));
    assert!(solutions.contains(&true));

    assert_eq!(solutions.len(), 2);
}

Branch number 0 then finds no alternative paths and exploration ends after one iteration.
Remove the let _irrelevant = a1.load(Relaxed); and exploration happens as expected.

The code in Execution is a bit suspect:

        let next = self.path.branch_thread(self.id, {
            self.threads.iter().map(|(i, th)| {
                if initial.is_none() && th.is_runnable() {
                    initial = Some(i);
                }

                if initial == Some(i) {
                    Thread::Active
                } else if th.is_yield() {
                    Thread::Yield
                } else if !th.is_runnable() {
                    Thread::Disabled
                } else {
                    Thread::Skip // <--- here
                }
            })
        });

This sets the thread to Skip even though it is runnable. However, changing this to Pending makes this case work, but breaks some other tests as some schedulings (e.g. thread creations?) count as preemptions, making loom eventually hit the assert in path.rs:

            debug_assert!(
                self.preemption_bound.is_none() || Some(preemptions) <= self.preemption_bound,
                "[loom internal bug] max = {:?}; curr = {}",
                self.preemption_bound,
                preemptions,
            );

I can't follow the code well enough yet to prepare a fix. Any pointers as to what should actually happen would be highly appreciated.

I guess a workaround is to start each spawned thread with some otherwise useless store?

Edit: Tried that, but the same issue occurs in some other circumstances as well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions