Repository: tokio-rs/loom (Rust)

Issue:
incorrect semantics for `Thread::unpark` followed by `thread::park`
If `std::thread::Thread::unpark` is called _prior_ to a call to `std::thread::park`, the `park` call will return immediately, "consuming" the previous unpark. Per [the documentation](https://doc.rust-lang.org/std/thread/fn.park.html#park-and-unpark):

> The `unpark` method on a `Thread` atomically makes the token available if it wasn’t already. Because the token is initially absent, `unpark` followed by `park` will result in the second call returning immediately.

Loom's version of this API does not have this behavior. It appears that `unpark` only notifies a thread if it is currently `park`ed, and does not influence the next call to `park`.

The following test demonstrates this:
```rust
#[test]
fn park_unpark_loom() {
    loom::model(|| {
        loom::thread::current().unpark();
        loom::thread::park();
        println!("it did not deadlock");
    });
}

#[test]
fn park_unpark_std() {
    std::thread::current().unpark();
    std::thread::park();
    println!("it did not deadlock");
}
```

The `park_unpark_std` test will run to completion and print "it did not deadlock", while the `park_unpark_loom` test panics with a deadlock detection error:

```
running 2 tests
test park_unpark_std ... ok
test park_unpark_loom ... FAILED

failures:

---- park_unpark_loom stdout ----
unpark
park
thread 'park_unpark_loom' panicked at 'deadlock; threads = [(Id(0), Blocked)]', src/rt/execution.rs:209:13
stack backtrace:
   ...
```


Discussion:
It looks like the `Notify` type has this behavior, but the `loom::thread` API does not. The `Notify` type's documentation explains that it can be used to mock out park/unpark primitives...which is nice, but sometimes the code being tested _is_ a park/unpark primitive. :)
I assume this is because `Thread::unpark` early returns and does nothing when called from its own thread. It should still set the threads token.

https://github.com/tokio-rs/loom/blob/555b52fdb267964f0950a52be87b0f28c40b054c/src/rt/thread.rs#L258-L261



Patch:
diff --git a/src/rt/mod.rs b/src/rt/mod.rs
index cdf07852..4a56e332 100644
--- a/src/rt/mod.rs
+++ b/src/rt/mod.rs
@@ -82,17 +82,32 @@ where
 
 /// Marks the current thread as blocked
 pub fn park() {
-    execution(|execution| {
+    let switch = execution(|execution| {
+        use thread::State;
         let thread = execution.threads.active_id();
+        let active = execution.threads.active_mut();
+
+        trace!(?thread, ?active.state, "park");
 
-        trace!(?thread, "park");
+        match active.state {
+            // The thread was previously unparked while it was active. Instead
+            // of parking, consume the unpark.
+            State::Runnable { unparked: true } => {
+                active.set_runnable();
+                return false;
+            }
+            // The thread doesn't have a saved unpark; set its state to blocked.
+            _ => active.set_blocked(),
+        };
 
         execution.threads.active_mut().set_blocked();
         execution.threads.active_mut().operation = None;
         execution.schedule()
     });
 
-    Scheduler::switch();
+    if switch {
+        Scheduler::switch();
+    }
 }
 
 /// Add an execution branch point.
diff --git a/src/rt/thread.rs b/src/rt/thread.rs
index 5fa8e2f3..391221dc 100644
--- a/src/rt/thread.rs
+++ b/src/rt/thread.rs
@@ -67,7 +67,7 @@ impl Id {
 
 #[derive(Debug, Clone, Copy)]
 pub(crate) enum State {
-    Runnable,
+    Runnable { unparked: bool },
     Blocked,
     Yield,
     Terminated,
@@ -84,7 +84,7 @@ impl Thread {
     fn new(id: Id) -> Thread {
         Thread {
             id,
-            state: State::Runnable,
+            state: State::Runnable { unparked: false },
             critical: false,
             operation: None,
             causality: VersionVec::new(),
@@ -97,11 +97,11 @@ impl Thread {
     }
 
     pub(crate) fn is_runnable(&self) -> bool {
-        matches!(self.state, State::Runnable)
+        matches!(self.state, State::Runnable { .. })
     }
 
     pub(crate) fn set_runnable(&mut self) {
-        self.state = State::Runnable;
+        self.state = State::Runnable { unparked: false };
     }
 
     pub(crate) fn set_blocked(&mut self) {
@@ -143,9 +143,16 @@ impl Thread {
 
     pub(crate) fn unpark(&mut self, unparker: &Thread) {
         self.causality.join(&unparker.causality);
+        self.set_unparked();
+    }
 
+    /// Unpark a thread's state. If it is already runnable, store the unpark for
+    /// a future call to `park`.
+    fn set_unparked(&mut self) {
         if self.is_blocked() || self.is_yield() {
             self.set_runnable();
+        } else if self.is_runnable() {
+            self.state = State::Runnable { unparked: true }
         }
     }
 }
@@ -257,6 +264,10 @@ impl Set {
 
     pub(crate) fn unpark(&mut self, id: Id) {
         if id == self.active_id() {
+            // The thread is unparking itself. We don't have to join its
+            // causality with the unparker's causality in this case, since the
+            // thread *is* the unparker. Just unpark its state.
+            self.active_mut().set_unparked();
             return;
         }
 


Test patch:
diff --git a/tests/thread_api.rs b/tests/thread_api.rs
index 168c5e1d..95405e48 100644
--- a/tests/thread_api.rs
+++ b/tests/thread_api.rs
@@ -103,3 +103,23 @@ fn thread_names() {
         let _ = th.join();
     })
 }
+
+#[test]
+fn park_unpark_loom() {
+    loom::model(|| {
+        println!("unpark");
+        thread::current().unpark();
+        println!("park");
+        thread::park();
+        println!("it did not deadlock");
+    });
+}
+
+#[test]
+fn park_unpark_std() {
+    println!("unpark");
+    std::thread::current().unpark();
+    println!("park");
+    std::thread::park();
+    println!("it did not deadlock");
+}