Skip to content

Commit 24e93a9

Browse files
feat: support thread parking (MVP)
1 parent 73de560 commit 24e93a9

20 files changed

+4104
-41
lines changed

.github/workflows/ci.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
--target ${{ env.NO_STD_TARGET }}
4444
--no-dev-deps
4545
--feature-powerset
46-
--skip yield,thread_local
46+
--skip yield,thread_local,parking
4747
4848
msrv:
4949
name: MSRV
@@ -108,6 +108,8 @@ jobs:
108108
run: cargo run --example thread_local --features thread_local
109109
- name: Run lock_api example
110110
run: cargo run --example lock_api --features lock_api,barging
111+
- name: Run parking with thread_local example
112+
run: cargo run --example parking --features parking,thread_local
111113

112114
linter:
113115
name: Linter

Cargo.toml

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ categories = ["algorithms", "concurrency", "no-std", "no-std::no-alloc"]
1717
keywords = ["mutex", "no_std", "spinlock", "synchronization"]
1818

1919
[features]
20-
# NOTE: Features `yield`, `thread_local` require std.
20+
# NOTE: Features `yield`, `thread_local` and `parking` require std.
2121
yield = []
2222
thread_local = []
2323
barging = []
2424
# NOTE: The `dep:` syntax requires Rust 1.60.
25+
parking = ["dep:atomic-wait"]
2526
lock_api = ["dep:lock_api"]
2627

28+
[dependencies.atomic-wait]
29+
version = "1"
30+
optional = true
31+
2732
[dependencies.lock_api]
2833
version = "0.4"
2934
default-features = false
@@ -44,6 +49,10 @@ check-cfg = ["cfg(loom)", "cfg(tarpaulin)", "cfg(tarpaulin_include)"]
4449
name = "barging"
4550
required-features = ["barging"]
4651

52+
[[example]]
53+
name = "parking"
54+
required-features = ["parking"]
55+
4756
[[example]]
4857
name = "thread_local"
4958
required-features = ["thread_local"]

Makefile.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ args = [
2323
"--feature-powerset",
2424
"--no-dev-deps",
2525
"--skip",
26-
"yield,thread_local",
26+
"yield,thread_local,parking",
2727
]
2828
dependencies = ["install-no-std-target"]
2929

README.md

+72-16
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
![No_std][no_std-badge]
1010

1111
MCS lock is a List-Based Queuing Lock that avoids network contention by having
12-
threads spin on local memory locations. The main properties of this mechanism are:
12+
threads spin and/or park on local memory locations. The main properties of this
13+
mechanism are:
1314

1415
- guarantees FIFO ordering of lock acquisitions;
1516
- spins on locally-accessible flag variables only;
@@ -24,9 +25,10 @@ paper. And a simpler correctness proof of the MCS lock was proposed by
2425
## Spinlock use cases
2526

2627
It is noteworthy to mention that [spinlocks are usually not what you want]. The
27-
majority of use cases are well covered by OS-based mutexes like [`std::sync::Mutex`]
28-
and [`parking_lot::Mutex`]. These implementations will notify the system that the
29-
waiting thread should be parked, freeing the processor to work on something else.
28+
majority of use cases are well covered by OS-based mutexes like
29+
[`std::sync::Mutex`] or [`parking_lot::Mutex`] or even this crate's [`parking`]
30+
Mutexes. These implementations will notify the system that the waiting thread
31+
should be parked, freeing the processor to work on something else.
3032

3133
Spinlocks are only efficient in very few circumstances where the overhead
3234
of context switching or process rescheduling are greater than busy waiting
@@ -160,6 +162,42 @@ fn main() {
160162
}
161163
```
162164

165+
## Locking with thread parking MCS locks
166+
167+
This crate also supports MCS lock implementations that will put the blocking
168+
threads to sleep. All `no_std` flavors: `raw`, `barging` have matching `Mutex`
169+
types under the [`parking`] module, with corresponding paths and public APIs,
170+
that are thread parking capable. These implementations are not `no_std`
171+
compatible. See [`parking`] module for more information.
172+
173+
```rust
174+
use std::sync::Arc;
175+
use std::thread;
176+
177+
// Requires `parking` feature.
178+
// Spins for a while then parks during contention.
179+
use mcslock::parking::raw::{spins::Mutex, MutexNode};
180+
181+
// Requires `parking` and `thread_local` features.
182+
mcslock::thread_local_parking_node!(static NODE);
183+
184+
fn main() {
185+
let mutex = Arc::new(Mutex::new(0));
186+
let c_mutex = Arc::clone(&mutex);
187+
188+
thread::spawn(move || {
189+
// Local node handles are provided by reference.
190+
// Critical section must be defined as a closure.
191+
c_mutex.lock_with_local_then(&NODE, |data| *data = 10);
192+
})
193+
.join().expect("thread::spawn failed");
194+
195+
// A node may also be transparently allocated in the stack.
196+
// Critical section must be defined as a closure.
197+
assert_eq!(mutex.try_lock_then(|data| *data.unwrap()), 10);
198+
}
199+
```
200+
163201
## Features
164202

165203
This crate dos not provide any default features. Features that can be enabled
@@ -178,26 +216,36 @@ just simply busy-waits. This feature is not `no_std` compatible.
178216

179217
### thread_local
180218

181-
The `thread_local` feature enables [`raw::Mutex`] locking APIs that operate over
182-
queue nodes that are stored at the thread local storage. These locking APIs
183-
require a static reference to [`raw::LocalMutexNode`] keys. Keys must be generated
184-
by the [`thread_local_node!`] macro. This feature also enables memory optimizations
185-
for [`barging::Mutex`] and locking operations. This feature is not `no_std`
219+
The `thread_local` feature enables [`raw::Mutex`] and [`parking::raw::Mutex`]
220+
locking APIs that operate over queue nodes that are stored at the thread local
221+
storage. These locking APIs require a static reference to [`raw::LocalMutexNode`]
222+
and [`parking::raw::LocalMutexNode`] keys respectively. Keys must be generated
223+
by the [`thread_local_node!`] and [`thread_local_parking_node!`] macros. This
224+
feature also enables memory optimizations for [`barging::Mutex`] and
225+
[`parking::barging::Mutex`] locking operations. This feature is not `no_std`
186226
compatible.
187227

188228
### barging
189229

190-
The `barging` feature provides locking APIs that are compatible with the
191-
[lock_api] crate. It does not require node allocations from the caller.
192-
The [`barging`] module is suitable for `no_std` environments. This implementation
193-
is not fair (does not guarantee FIFO), but can improve throughput when the lock
194-
is heavily contended.
230+
The `barging` feature provides locking APIs that are compatible with the [lock_api]
231+
crate. It does not require node allocations from the caller. The [`barging`] module
232+
is suitable for `no_std` environments, but [`parking::barging`] is not. This
233+
implementation is not fair (does not guarantee FIFO), but can improve throughput
234+
when the lock is heavily contended.
195235

196236
### lock_api
197237

198238
This feature implements the [`RawMutex`] trait from the [lock_api] crate for
199-
[`barging::Mutex`]. Aliases are provided by the [`barging::lock_api`] (`no_std`)
200-
module.
239+
both [`barging::Mutex`] and [`parking::barging::Mutex`]. Aliases are provided by
240+
the [`barging::lock_api`] (`no_std`) and [`parking::barging::lock_api`] modules.
241+
242+
### parking
243+
244+
The `parking` feature provides Mutex implementations that are capable of putting
245+
blocking threads waiting for the lock to sleep. These implementations are
246+
published under the [`parking`] module. Each `no_std` mutex flavors provided
247+
by this crate have corresponding parking implementations under that module.
248+
Users may select a out of the box parking policy at [`parking::park`].
201249

202250
## Minimum Supported Rust Version (MSRV)
203251

@@ -258,10 +306,18 @@ each of your dependencies, including this one.
258306
[`raw::Mutex`]: https://docs.rs/mcslock/latest/mcslock/raw/struct.Mutex.html
259307
[`raw::MutexNode`]: https://docs.rs/mcslock/latest/mcslock/raw/struct.MutexNode.html
260308
[`raw::LocalMutexNode`]: https://docs.rs/mcslock/latest/mcslock/raw/struct.LocalMutexNode.html
309+
[`parking`]: https://docs.rs/mcslock/latest/mcslock/parking/index.html
310+
[`parking::park`]: https://docs.rs/mcslock/latest/mcslock/parking/park/index.html
311+
[`parking::barging`]: https://docs.rs/mcslock/latest/mcslock/parking/barging/index.html
312+
[`parking::lock_api`]: https://docs.rs/mcslock/latest/mcslock/parking/lock_api/index.html
313+
[`parking::raw::Mutex`]: https://docs.rs/mcslock/latest/mcslock/parking/raw/struct.Mutex.html
314+
[`parking::raw::LocalMutexNode`]: https://docs.rs/mcslock/latest/mcslock/parking/raw/struct.LocalMutexNode.html
315+
[`parking::barging::Mutex`]: https://docs.rs/mcslock/latest/mcslock/parking/barging/struct.Mutex.html
261316
[`barging`]: https://docs.rs/mcslock/latest/mcslock/barging/index.html
262317
[`barging::lock_api`]: https://docs.rs/mcslock/latest/mcslock/barging/lock_api/index.html
263318
[`barging::Mutex`]: https://docs.rs/mcslock/latest/mcslock/barging/struct.Mutex.html
264319
[`thread_local_node!`]: https://docs.rs/mcslock/latest/mcslock/macro.thread_local_node.html
320+
[`thread_local_parking_node!`]: https://docs.rs/mcslock/latest/mcslock/macro.thread_local_parking_node.html
265321

266322
[`std::sync::Mutex`]: https://doc.rust-lang.org/std/sync/struct.Mutex.html
267323
[`std::thread::yield_now`]: https://doc.rust-lang.org/std/thread/fn.yield_now.html

examples/parking.rs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use std::sync::mpsc::channel;
2+
use std::sync::Arc;
3+
use std::thread;
4+
5+
// Requires `parking` feature.
6+
// `spins::Mutex` spins for a while then parks during contention.
7+
use mcslock::parking::raw::{spins::Mutex, MutexNode};
8+
9+
fn main() {
10+
const N: usize = 10;
11+
12+
// Spawn a few threads to increment a shared variable (non-atomically), and
13+
// let the main thread know once all increments are done.
14+
//
15+
// Here we're using an Arc to share memory among threads, and the data inside
16+
// the Arc is protected with a mutex.
17+
let data = Arc::new(Mutex::new(0));
18+
19+
let (tx, rx) = channel();
20+
for _ in 0..N {
21+
let (data, tx) = (data.clone(), tx.clone());
22+
thread::spawn(move || {
23+
// A queue node must be mutably accessible.
24+
let mut node = MutexNode::new();
25+
// The shared state can only be accessed once the lock is held.
26+
// Our non-atomic increment is safe because we're the only thread
27+
// which can access the shared state when the lock is held.
28+
//
29+
// We unwrap() the return value to assert that we are not expecting
30+
// threads to ever fail while holding the lock.
31+
data.lock_with_then(&mut node, |data| {
32+
*data += 1;
33+
if *data == N {
34+
tx.send(()).unwrap();
35+
}
36+
// The lock is unlocked here at the end of the closure scope.
37+
});
38+
// The node can now be reused for other locking operations.
39+
let _ = data.lock_with_then(&mut node, |data| *data);
40+
});
41+
}
42+
let _message = rx.recv();
43+
44+
// A queue node is transparently allocated in the stack.
45+
let count = data.lock_then(|data| *data);
46+
assert_eq!(count, N);
47+
}

src/lib.rs

+72-18
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
//! contention-free [lock] for mutual exclusion, referred to as MCS lock.
33
//!
44
//! MCS lock is a List-Based Queuing Lock that avoids network contention by
5-
//! having threads spin on local memory locations. The main properties of this
6-
//! mechanism are:
5+
//! having threads spin and/or park on local memory locations. The main
6+
//! properties of this mechanism are:
77
//!
88
//! - guarantees FIFO ordering of lock acquisitions;
99
//! - spins on locally-accessible flag variables only;
@@ -19,9 +19,9 @@
1919
//!
2020
//! It is noteworthy to mention that [spinlocks are usually not what you want].
2121
//! The majority of use cases are well covered by OS-based mutexes like
22-
//! [`std::sync::Mutex`], [`parking_lot::Mutex`]. These implementations will
23-
//! notify the system that the waiting thread should be parked, freeing the
24-
//! processor to work on something else.
22+
//! [`std::sync::Mutex`], [`parking_lot::Mutex`] or even this crate's [`parking`]
23+
//! Mutexes. These implementations will notify the system that the waiting thread
24+
//! should be parked, freeing the processor to work on something else.
2525
//!
2626
//! Spinlocks are only efficient in very few circunstances where the overhead
2727
//! of context switching or process rescheduling are greater than busy waiting
@@ -133,6 +133,45 @@
133133
//! # fn main() {}
134134
//! ```
135135
//!
136+
//! ## Locking with thread parking MCS locks
137+
//!
138+
//! This crate also supports MCS lock implementations that will put the blocking
139+
//! threads to sleep. All `no_std` flavors: `raw`, `barging` have matching `Mutex`
140+
//! types under the [`parking`] module, with corresponding paths and public APIs,
141+
//! that are thread parking capable. These implementations are not `no_std`
142+
//! compatible. See [`parking`] module for more information.
143+
//!
144+
//! ```
145+
//! # #[cfg(all(feature = "thread_local", feature = "parking"))]
146+
//! # {
147+
//! use std::sync::Arc;
148+
//! use std::thread;
149+
//!
150+
//! // Requires `parking` feature.
151+
//! // Spins for a while then parks during contention.
152+
//! use mcslock::parking::raw::{spins::Mutex, MutexNode};
153+
//!
154+
//! // Requires `parking` and `thread_local` features.
155+
//! mcslock::thread_local_parking_node!(static NODE);
156+
//!
157+
//! let mutex = Arc::new(Mutex::new(0));
158+
//! let c_mutex = Arc::clone(&mutex);
159+
//!
160+
//! thread::spawn(move || {
161+
//! // Local node handles are provided by reference.
162+
//! // Critical section must be defined as a closure.
163+
//! c_mutex.lock_with_local_then(&NODE, |data| *data = 10);
164+
//! })
165+
//! .join().expect("thread::spawn failed");
166+
//!
167+
//! // A node may also be transparently allocated in the stack.
168+
//! // Critical section must be defined as a closure.
169+
//! assert_eq!(mutex.try_lock_then(|data| *data.unwrap()), 10);
170+
//! # }
171+
//! # #[cfg(not(all(feature = "thread_local", feature = "parking")))]
172+
//! # fn main() {}
173+
//! ```
174+
//!
136175
//! ## Features
137176
//!
138177
//! This crate dos not provide any default features. Features that can be enabled
@@ -151,28 +190,40 @@
151190
//!
152191
//! ### thread_local
153192
//!
154-
//! The `thread_local` feature enables [`raw::Mutex`] locking APIs that operate
155-
//! over queue nodes that are stored at the thread local storage. These locking
156-
//! APIs require a static reference to [`raw::LocalMutexNode`] keys. Keys must be
157-
//! generated by the [`thread_local_node!`] macro. This feature also enables memory
158-
//! optimizations for [`barging::Mutex`] locking operations. This feature is not
159-
//! `no_std` compatible.
193+
//! The `thread_local` feature enables [`raw::Mutex`] and [`parking::raw::Mutex`]
194+
//! locking APIs that operate over queue nodes that are stored at the thread
195+
//! local storage. These locking APIs require a static reference to
196+
//! [`raw::LocalMutexNode`] and [`parking::raw::LocalMutexNode`] keys respectively.
197+
//! Keys must be generated by the [`thread_local_node!`] and
198+
//! [`thread_local_parking_node`] macros. This feature also enables memory
199+
//! optimizations for [`barging::Mutex`] and [`parking::barging::Mutex`] locking
200+
//! operations. This feature is not `no_std` compatible.
160201
//!
161202
//! This feature is not `no_std` compatible.
162203
//!
163204
//! ### barging
164205
//!
165206
//! The `barging` feature provides locking APIs that are compatible with the
166207
//! [lock_api] crate. It does not require node allocations from the caller. The
167-
//! [`barging`] module is suitable for `no_std` environments. This implementation
168-
//! is not fair (does not guarantee FIFO), but can improve throughput when the lock
169-
//! is heavily contended.
208+
//! [`barging`] module is suitable for `no_std` environments, but
209+
//! [`parking::barging`] is not. These implementations are not fair (they do
210+
//! not guarantee FIFO), but can improve throughput when the lock is heavily
211+
//! contended.
170212
//!
171213
//! ### lock_api
172214
//!
173215
//! This feature implements the [`RawMutex`] trait from the [lock_api]
174-
//! crate for [`barging::Mutex`]. Aliases are provided by the [`barging::lock_api`]
175-
//! (`no_std`) module.
216+
//! crate for both [`barging::Mutex`] and [`parking::barging::Mutex`]. Aliases
217+
//! are provided by the [`barging::lock_api`] (`no_std`) and
218+
//! [`parking::barging::lock_api`] modules.
219+
//!
220+
//! ### parking
221+
//!
222+
//! The `parking` feature provides Mutex implementations that are capable of
223+
//! putting blocking threads waiting for the lock to sleep. These implementations
224+
//! are published under the [`parking`] module. Each `no_std` mutex flavors
225+
//! provided by this crate have corresponding parking implementations under that
226+
//! module. Users may select a out of the box parking policy at [`parking::park`].
176227
//!
177228
//! ## Related projects
178229
//!
@@ -193,7 +244,6 @@
193244
//! [lock_api]: https://docs.rs/lock_api/latest/lock_api
194245
//! [`RawMutex`]: https://docs.rs/lock_api/latest/lock_api/trait.RawMutex.html
195246
//! [`RawMutexFair`]: https://docs.rs/lock_api/latest/lock_api/trait.RawMutexFair.html
196-
//! [`parking_lot::Mutex`]: https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html
197247
198248
#![no_std]
199249
#![allow(clippy::doc_markdown)]
@@ -203,7 +253,7 @@
203253
#![warn(missing_docs)]
204254
#![warn(rust_2024_compatibility)]
205255

206-
#[cfg(any(feature = "yield", feature = "thread_local", loom, test))]
256+
#[cfg(any(feature = "yield", feature = "thread_local", feature = "parking", loom, test))]
207257
extern crate std;
208258

209259
#[cfg(feature = "thread_local")]
@@ -221,6 +271,10 @@ pub(crate) mod lock;
221271
#[cfg_attr(docsrs, doc(cfg(feature = "barging")))]
222272
pub mod barging;
223273

274+
#[cfg(feature = "parking")]
275+
#[cfg_attr(docsrs, doc(cfg(feature = "parking")))]
276+
pub mod parking;
277+
224278
#[cfg(test)]
225279
pub(crate) mod test;
226280

0 commit comments

Comments
 (0)