Skip to content

Commit 58ae5b7

Browse files
committed
thread parking: fix docs and examples
1 parent 5ab6924 commit 58ae5b7

File tree

4 files changed

+62
-16
lines changed

4 files changed

+62
-16
lines changed

library/std/src/sys/sync/once/queue.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ fn wait(
276276
// If the managing thread happens to signal and unpark us before we
277277
// can park ourselves, the result could be this thread never gets
278278
// unparked. Luckily `park` comes with the guarantee that if it got
279-
// an `unpark` just before on an unparked thread it does not park.
279+
// an `unpark` just before on an unparked thread it does not park. Crucially, we know
280+
// the `unpark` must have happened between the `compare_exchange_weak` above and here,
281+
// and there's no other `park` in that code that could steal our token.
280282
// SAFETY: we retrieved this handle on the current thread above.
281283
unsafe { node.thread.park() }
282284
}

library/std/src/thread/mod.rs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,13 +1021,23 @@ impl Drop for PanicGuard {
10211021
/// specifying a maximum time to block the thread for.
10221022
///
10231023
/// * The [`unpark`] method on a [`Thread`] atomically makes the token available
1024-
/// if it wasn't already. Because the token is initially absent, [`unpark`]
1025-
/// followed by [`park`] will result in the second call returning immediately.
1026-
///
1027-
/// The API is typically used by acquiring a handle to the current thread,
1028-
/// placing that handle in a shared data structure so that other threads can
1029-
/// find it, and then `park`ing in a loop. When some desired condition is met, another
1030-
/// thread calls [`unpark`] on the handle.
1024+
/// if it wasn't already. Because the token can be held by a thread even if it is currently not
1025+
/// parked, [`unpark`] followed by [`park`] will result in the second call returning immediately.
1026+
/// However, note that to rely on this guarantee, you need to make sure that your `unpark` happens
1027+
/// after all `park` that may be done by other data structures!
1028+
///
1029+
/// The API is typically used by acquiring a handle to the current thread, placing that handle in a
1030+
/// shared data structure so that other threads can find it, and then `park`ing in a loop. When some
1031+
/// desired condition is met, another thread calls [`unpark`] on the handle. The last bullet point
1032+
/// above guarantees that even if the `unpark` occurs before the thread is finished `park`ing, it
1033+
/// will be woken up properly.
1034+
///
1035+
/// Note that the coordination via the shared data structure is crucial: If you `unpark` a thread
1036+
/// without first establishing that it is about to be `park`ing within your code, that `unpark` may
1037+
/// get consumed by a *different* `park` in the same thread, leading to a deadlock. This also means
1038+
/// you must not call unknown code between setting up for parking and calling `park`; for instance,
1039+
/// if you invoke `println!`, that may itself call `park` and thus consume your `unpark` and cause a
1040+
/// deadlock.
10311041
///
10321042
/// The motivation for this design is twofold:
10331043
///
@@ -1058,33 +1068,49 @@ impl Drop for PanicGuard {
10581068
///
10591069
/// ```
10601070
/// use std::thread;
1061-
/// use std::sync::{Arc, atomic::{Ordering, AtomicBool}};
1071+
/// use std::sync::atomic::{Ordering, AtomicBool};
10621072
/// use std::time::Duration;
10631073
///
1064-
/// let flag = Arc::new(AtomicBool::new(false));
1065-
/// let flag2 = Arc::clone(&flag);
1074+
/// static QUEUED: AtomicBool = AtomicBool::new(false);
1075+
/// static FLAG: AtomicBool = AtomicBool::new(false);
10661076
///
10671077
/// let parked_thread = thread::spawn(move || {
1078+
/// println!("Thread spawned");
1079+
/// // Signal that we are going to `park`. Between this store and our `park`, there may
1080+
/// // be no other `park`, or else that `park` could consume our `unpark` token!
1081+
/// QUEUED.store(true, Ordering::Release);
10681082
/// // We want to wait until the flag is set. We *could* just spin, but using
10691083
/// // park/unpark is more efficient.
1070-
/// while !flag2.load(Ordering::Relaxed) {
1071-
/// println!("Parking thread");
1084+
/// while !FLAG.load(Ordering::Acquire) {
1085+
/// // `eprintln!` does not do any thread parking internally so we can safely call it here.
1086+
/// eprintln!("Parking thread");
10721087
/// thread::park();
10731088
/// // We *could* get here spuriously, i.e., way before the 10ms below are over!
10741089
/// // But that is no problem, we are in a loop until the flag is set anyway.
1075-
/// println!("Thread unparked");
1090+
/// eprintln!("Thread unparked");
10761091
/// }
10771092
/// println!("Flag received");
10781093
/// });
10791094
///
10801095
/// // Let some time pass for the thread to be spawned.
10811096
/// thread::sleep(Duration::from_millis(10));
10821097
///
1098+
/// // Ensure the thread is about to park.
1099+
/// // This is crucial! It guarantees that the `unpark` below is not consumed
1100+
/// // by some other code in the parked thread (e.g. inside `println!`).
1101+
/// while !QUEUED.load(Ordering::Acquire) {
1102+
/// // Spinning is of course inefficient; in practice, this would more likely be
1103+
/// // a dequeue where we have no work to do if there's nobody queued.
1104+
/// std::hint::spin_loop();
1105+
/// }
1106+
///
10831107
/// // Set the flag, and let the thread wake up.
1084-
/// // There is no race condition here, if `unpark`
1108+
/// // There is no race condition here: if `unpark`
10851109
/// // happens first, `park` will return immediately.
1110+
/// // There is also no other `park` that could consume this token,
1111+
/// // since we waited until the other thread got queued.
10861112
/// // Hence there is no risk of a deadlock.
1087-
/// flag.store(true, Ordering::Relaxed);
1113+
/// FLAG.store(true, Ordering::Release);
10881114
/// println!("Unpark the thread");
10891115
/// parked_thread.thread().unpark();
10901116
///
@@ -1494,10 +1520,14 @@ impl Thread {
14941520
/// ```
14951521
/// use std::thread;
14961522
/// use std::time::Duration;
1523+
/// use std::sync::atomic::{AtomicBool, Ordering};
1524+
///
1525+
/// static QUEUED: AtomicBool = AtomicBool::new(false);
14971526
///
14981527
/// let parked_thread = thread::Builder::new()
14991528
/// .spawn(|| {
15001529
/// println!("Parking thread");
1530+
/// QUEUED.store(true, Ordering::Release);
15011531
/// thread::park();
15021532
/// println!("Thread unparked");
15031533
/// })
@@ -1506,6 +1536,15 @@ impl Thread {
15061536
/// // Let some time pass for the thread to be spawned.
15071537
/// thread::sleep(Duration::from_millis(10));
15081538
///
1539+
/// // Wait until the other thread is queued.
1540+
/// // This is crucial! It guarantees that the `unpark` below is not consumed
1541+
/// // by some other code in the parked thread (e.g. inside `println!`).
1542+
/// while !QUEUED.load(Ordering::Acquire) {
1543+
/// // Spinning is of course inefficient; in practice, this would more likely be
1544+
/// // a dequeue where we have no work to do if there's nobody queued.
1545+
/// std::hint::spin_loop();
1546+
/// }
1547+
///
15091548
/// println!("Unpark the thread");
15101549
/// parked_thread.thread().unpark();
15111550
///

library/std/src/thread/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ fn test_park_unpark_called_other_thread() {
287287
for _ in 0..10 {
288288
let th = thread::current();
289289

290+
// Here we rely on `thread::spawn` (specifically the part that runs after spawning
291+
// the thread) to not consume the parking token.
290292
let _guard = thread::spawn(move || {
291293
super::sleep(Duration::from_millis(50));
292294
th.unpark();
@@ -316,6 +318,8 @@ fn test_park_timeout_unpark_called_other_thread() {
316318
for _ in 0..10 {
317319
let th = thread::current();
318320

321+
// Here we rely on `thread::spawn` (specifically the part that runs after spawning
322+
// the thread) to not consume the parking token.
319323
let _guard = thread::spawn(move || {
320324
super::sleep(Duration::from_millis(50));
321325
th.unpark();

src/tools/miri/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ Definite bugs found:
600600
* [A bug in the new `RwLock::downgrade` implementation](https://rust-lang.zulipchat.com/#narrow/channel/269128-miri/topic/Miri.20error.20library.20test) (caught by Miri before it landed in the Rust repo)
601601
* [Mockall reading uninitialized memory when mocking `std::io::Read::read`, even if all expectations are satisfied](https://github.com/asomers/mockall/issues/647) (caught by Miri running Tokio's test suite)
602602
* [`ReentrantLock` not correctly dealing with reuse of addresses for TLS storage of different threads](https://github.com/rust-lang/rust/pull/141248)
603+
* [Rare Deadlock in the thread (un)parking example code](https://github.com/rust-lang/rust/issues/145816)
603604

604605
Violations of [Stacked Borrows] found that are likely bugs (but Stacked Borrows is currently just an experiment):
605606

0 commit comments

Comments
 (0)