`thread::unpark()` bug?

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::thread;
use std::thread::Thread;
use tracing::{info, Level};

mod pool;
mod ring;

pub struct Test {
    wait_list: Mutex<VecDeque<Thread>>,
}

impl Test {
    pub fn new() -> Self {
        Self {
            wait_list: Mutex::new(VecDeque::new()),
        }
    }

    pub fn f1(&self) {
        {
            let mut list = self.wait_list.lock().unwrap();
            if let Some(t) = list.pop_front() {
                t.unpark();
                info!("call unpark");
            }
        }
    }

    pub fn f2(&self) {
        let t = thread::current();
        {
            self.wait_list.lock().unwrap().push_back(t);
        }
        info!("park");
        thread::park();
        info!("unpark");
    }
}

fn main() {
    tracing_subscriber::fmt()
        .event_format(
            tracing_subscriber::fmt::format()
                .with_line_number(true)
                .with_level(true)
                .with_target(true),
        )
        .with_max_level(Level::INFO)
        .try_init()
        .unwrap();
    let t = Arc::new(Test::new());
    let t1 = t.clone();
    thread::spawn(move || {
        t1.f1();
    });
    let t2 = t.clone();
    thread::spawn(move || {
        t2.f1();
    });
    t.f2();
}

Why thread::unpark() lost for caller thread?

The porpose of this code is to park thread which call Test::f2() and two child threads call Test::f1() to unpark main thread(which one got access to mutex do wake logic first and I create 2 threads to simulate race condition).

But I got output like:


Says the thread::unpark() is invoked, but parking thread didn't wake up.

the first 3 lines output is expected, and last 2 lines is wrong result.

My machine is MacBookPro2023, SOC is M2Max, Rust Version is 1.71.0

1 Like

Your program has a race condition. Imagine this execution:

  • The main thread pushes itself into the queue
  • A thread pops from the list and tries to unpark it, but the main thread is not parked yet
  • The main thread now arrives at the thread::park() instruction and gets parked

After this sequence of instructions the main thread is parked, but it's no longer in the queue so no thread can/will ever unpark it.

You might be interested in the event-listener crate, which should handle this case.

4 Likes

What you mean is if a unpark() call is made first, then the following park() for same thread still park?

fn main() {
    let t = thread::current();
    thread::spawn(move || {
        t.unpark();
        println!("unpark called");
    });
    thread::sleep(std::time::Duration::from_secs(1));
    println!("parking");
    thread::park();
    println!("done");
}

But code above can still end. and I found that park() and unpark() using atomic counter to record wake up signal.

By the way, I have replaced thread::park() and unpark() with parking crate, and this problem is gone.

1 Like

You can check the std::sys::thread_parking::Packer implementation for MacOS here, pack should immediately return after unpack is called. So now I am totally confused as well.

1 Like

Does the code hang in the "wrong" case?

The problem is that your threads are racing. But it's not that unpark() is happening before park(). That should still work according to the documentation in the std library.

The problem is that there is every chance that both threads that you spawned ran and completed without doing anything before your main thread ever calls f2().

Since you're no doing any synchronization between the threads, they could possibly run in any order. And the order might be different each time you run it. So could get different results each time.

I'm not exactly sure what your use case is, but one thing you might want to do is have f1() block and wait for an item to be added to the deque. The classic way to do this is with a condition variable, which in std Rust is std::sync::Condvar.

That way, at least one or both of your spawned threads will be waiting for f2() to be called by main.

The twist is that f2() would need to add the current thread to the deque, signal the condition variable, unlock the mutex, and then park. You obviously must release the mutex before you park, since you can't go to sleep while still holding the mutex, but it creates the race of unpark possibly happening first before the park.

But, again, that is supposed to be ok, and my testing bears it out.

See

Again, I'm not sure why you're trying to create and maintain your own wait list. It may be cleaner to just use a Mutex and Condition Variable around whatever you're attempting to control and let their internal wait lists manage access to the shared resource.

That doesn't explain why "call unpark" is printed though.

Agreed. I didn’t answer the actual question!

It appears that one call to f1() must have received the thread id and signaled it (unpark) before the attempt to park it. By the documentation, this should work, and it worked for me on my particular Linux setup.

So I’m wondering if there are platform differences, or just some issues I’m not seeing?

But my point was that unless someone is trying to do something really advanced in trying to manage a wait list manually, there’s probably a better way to accomplish the intended goal. Use channels, condition variables, etc.

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.