Skip to content

feat: barging with thread local queue nodes #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
- name: Set Rust 1.65.0 as default
run: rustup default nightly-2022-09-18
- name: Check MSRV
env:
RUSTFLAGS: --allow unknown_lints
run: cargo check --all-features

docsrs:
Expand Down
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/target
/.helix
/Cargo.lock
/cobertura.xml
/cliff.toml
/cobertura.xml
/.helix
/Pipfile
/target
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ keywords = ["mutex", "no_std", "spinlock", "synchronization"]
yield = []
thread_local = []
barging = []
# NOTE: The `dep:` syntax requires Rust 1.60.
lock_api = ["dep:lock_api"]

[dependencies.lock_api]
Expand Down
30 changes: 25 additions & 5 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ CLIPPY_FLAGS_LOOM = "${CFG_LOOM} ${CLIPPY_FLAGS}"
[config]
default_to_workspace = false

# Install a "no_std" target.
[tasks.install-no-std-target]
command = "rustup"
args = ["target", "add", "thumbv7m-none-eabi"]

# Build package for no_std environment.
[tasks.no-std]
command = "cargo"
args = ["hack", "build", "--target", "thumbv7m-none-eabi", "--feature-powerset",
"--no-dev-deps", "--skip", "yield,thread_local"]
args = [
"hack",
"build",
"--target",
"thumbv7m-none-eabi",
"--feature-powerset",
"--no-dev-deps",
"--skip",
"yield,thread_local",
]
dependencies = ["install-no-std-target"]

# Build docs for docs.rs.
[tasks.docsrs]
Expand All @@ -28,7 +42,7 @@ args = ["rustdoc", "--all-features", "--open", "--", "--default-theme", "ayu"]
# Link: https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html
toolchain = "nightly-2022-09-18"
command = "cargo"
env = { "CARGO_UNSTABLE_SPARSE_REGISTRY" = "true" }
env = { "CARGO_UNSTABLE_SPARSE_REGISTRY" = "true", "RUSTFLAGS" = "--allow unknown_lints" }
args = ["check", "--all-features"]

# Check semver viloations.
Expand Down Expand Up @@ -57,8 +71,14 @@ args = ["run", "--example", "${@}", "--all-features"]
[tasks.test-lint]
command = "cargo"
env = { "RUSTFLAGS" = "${CLIPPY_FLAGS}" }
args = ["hack", "clippy", "--profile", "test", "--feature-powerset",
"--no-dev-deps"]
args = [
"hack",
"clippy",
"--profile",
"test",
"--feature-powerset",
"--no-dev-deps",
]

# Run tests under miri.
# NOTE: must run as: `cargo +nightly make miri`.
Expand Down
57 changes: 28 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Or add a entry under the `[dependencies]` section in your `Cargo.toml`:
# Cargo.toml

[dependencies]
# Available features: `yield`, `barging`, `thread_local` and `lock_api`.
# Features: `yield`, `barging`, `thread_local` and `lock_api`.
mcslock = { version = "0.4", features = ["thread_local"] }
```

Expand All @@ -75,7 +75,7 @@ module for more information.
use std::sync::Arc;
use std::thread;

// `spins::Mutex` simply spins during contention.
// Simply spins during contention.
use mcslock::raw::{spins::Mutex, MutexNode};

fn main() {
Expand All @@ -86,31 +86,29 @@ fn main() {
// A queue node must be mutably accessible.
// Critical section must be defined as a closure.
let mut node = MutexNode::new();
c_mutex.lock_with_then(&mut node, |data| {
*data = 10;
});
c_mutex.lock_with_then(&mut node, |data| *data = 10);
})
.join().expect("thread::spawn failed");

// A node is transparently allocated in the stack.
// A node may also be transparently allocated in the stack.
// Critical section must be defined as a closure.
assert_eq!(*mutex.try_lock_then(|data| *data.unwrap()), 10);
assert_eq!(mutex.try_lock_then(|data| *data.unwrap()), 10);
}
```

## Thread local queue nodes

Enables [`raw::Mutex`] locking APIs that operate over queue nodes that are
stored at the thread local storage. These locking APIs require a static
reference to a [`raw::LocalMutexNode`] key. Keys must be generated by the
[`thread_local_node!`] macro. Thread local nodes are not `no_std` compatible
and can be enabled through the `thread_local` feature.
[`raw::Mutex`] supports locking APIs that access queue nodes that are stored in
the thread local storage. These locking APIs require a static reference to a
[`raw::LocalMutexNode`] key. Keys must be generated by the [`thread_local_node!`]
macro. Thread local nodes are not `no_std` compatible and can be enabled through
the `thread_local` feature.

```rust
use std::sync::Arc;
use std::thread;

// `spins::Mutex` simply spins during contention.
// Simply spins during contention.
use mcslock::raw::spins::Mutex;

// Requires `thread_local` feature.
Expand All @@ -127,9 +125,9 @@ fn main() {
})
.join().expect("thread::spawn failed");

// Local node handles are provided by reference.
// A node may also be transparently allocated in the stack.
// Critical section must be defined as a closure.
assert_eq!(mutex.try_lock_with_local_then(&NODE, |data| *data.unwrap()), 10);
assert_eq!(mutex.try_lock_then(|data| *data.unwrap()), 10);
}
```

Expand All @@ -146,19 +144,19 @@ use std::sync::Arc;
use std::thread;

// Requires `barging` feature.
// `spins::backoff::Mutex` spins with exponential backoff during contention.
// Spins with exponential backoff during contention.
use mcslock::barging::spins::backoff::Mutex;

fn main() {
let mutex = Arc::new(Mutex::new(0));
let c_mutex = Arc::clone(&mutex);

thread::spawn(move || {
*c_mutex.lock() = 10;
*c_mutex.try_lock().unwrap() = 10;
})
.join().expect("thread::spawn failed");

assert_eq!(*mutex.try_lock().unwrap(), 10);
assert_eq!(*mutex.lock(), 10);
}
```

Expand All @@ -176,21 +174,24 @@ of busy-waiting during lock acquisitions and releases, this will call
OS scheduler. This may cause a context switch, so you may not want to enable
this feature if your intention is to to actually do optimistic spinning. The
default implementation calls [`core::hint::spin_loop`], which does in fact
just simply busy-waits. This feature **is not** `no_std` compatible.
just simply busy-waits. This feature is not `no_std` compatible.

### thread_local

The `thread_local` feature enables [`raw::Mutex`] locking APIs that operate over
queue nodes that are stored at the thread local storage. These locking APIs
require a static reference to a [`raw::LocalMutexNode`] key. Keys must be generated
by the [`thread_local_node!`] macro. This feature **is not** `no_std` compatible.
require a static reference to [`raw::LocalMutexNode`] keys. Keys must be generated
by the [`thread_local_node!`] macro. This feature also enables memory optimizations
for [`barging::Mutex`] and locking operations. This feature is not `no_std`
compatible.

### barging

The `barging` feature provides locking APIs that are compatible with the [lock_api]
crate. It does not require node allocations from the caller. The [`barging`] module
is suitable for `no_std` environments. This implementation **is not** fair (does not
guarantee FIFO), but can improve throughput when the lock is heavily contended.
The `barging` feature provides locking APIs that are compatible with the
[lock_api] crate. It does not require node allocations from the caller.
The [`barging`] module is suitable for `no_std` environments. This implementation
is not fair (does not guarantee FIFO), but can improve throughput when the lock
is heavily contended.

### lock_api

Expand All @@ -208,9 +209,8 @@ this crate MSRV substantially, it just has not been explored yet.

## Related projects

These projects provide MCS lock implementations with slightly different APIs,
implementation details or compiler requirements, you can check their
repositories:
These projects provide MCS lock implementations with different APIs, capabilities,
implementation details or compiler requirements, you can check their repositories:

- mcs-rs: <https://github.com/gereeter/mcs-rs>
- libmcs: <https://github.com/topecongiro/libmcs>
Expand Down Expand Up @@ -270,4 +270,3 @@ each of your dependencies, including this one.
[lock_api]: https://docs.rs/lock_api/latest/lock_api
[`RawMutex`]: https://docs.rs/lock_api/latest/lock_api/trait.RawMutex.html
[`RawMutexFair`]: https://docs.rs/lock_api/latest/lock_api/trait.RawMutexFair.html
[`parking_lot::Mutex`]: https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html
2 changes: 1 addition & 1 deletion rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
edition = "2021"
edition = "2024"
use_small_heuristics = "Max"
30 changes: 20 additions & 10 deletions src/barging/lock_api/mutex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::barging;
#[cfg(test)]
use crate::relax::Relax;
#[cfg(test)]
use crate::test::{LockData, LockNew, LockThen, TryLockThen};
use crate::test::{LockData, LockNew, LockThen, LockWithThen, TryLockThen, TryLockWithThen};

/// A [`lock_api::Mutex`] alias that wraps a [`barging::Mutex`].
///
Expand All @@ -28,27 +28,31 @@ impl<T: ?Sized, Rs: Relax, Rq: Relax> LockNew for Mutex<T, Rs, Rq> {
}

#[cfg(test)]
impl<T: ?Sized, Rs: Relax, Rq: Relax> LockThen for Mutex<T, Rs, Rq> {
type Guard<'a> = &'a mut Self::Target
impl<T: ?Sized, Rs: Relax, Rq: Relax> LockWithThen for Mutex<T, Rs, Rq> {
// The barging mutex does not require queue node access.
type Node = ();

type Guard<'a>
= MutexGuard<'a, T, Rs, Rq>
where
Self: 'a,
Self::Target: 'a;

fn lock_then<F, Ret>(&self, f: F) -> Ret
fn lock_with_then<F, Ret>(&self, (): &mut Self::Node, f: F) -> Ret
where
F: FnOnce(&mut Self::Target) -> Ret,
F: FnOnce(MutexGuard<'_, T, Rs, Rq>) -> Ret,
{
f(&mut *self.lock())
f(self.lock())
}
}

#[cfg(test)]
impl<T: ?Sized, Rs: Relax, Rq: Relax> TryLockThen for Mutex<T, Rs, Rq> {
fn try_lock_then<F, Ret>(&self, f: F) -> Ret
impl<T: ?Sized, Rs: Relax, Rq: Relax> TryLockWithThen for Mutex<T, Rs, Rq> {
fn try_lock_with_then<F, Ret>(&self, (): &mut Self::Node, f: F) -> Ret
where
F: FnOnce(Option<&mut Self::Target>) -> Ret,
F: FnOnce(Option<MutexGuard<'_, T, Rs, Rq>>) -> Ret,
{
f(self.try_lock().as_deref_mut())
f(self.try_lock())
}

fn is_locked(&self) -> bool {
Expand All @@ -70,6 +74,12 @@ impl<T: ?Sized, Rs: Relax, Rq: Relax> LockData for Mutex<T, Rs, Rq> {
}
}

#[cfg(test)]
impl<T: ?Sized, Rs: Relax, Rq: Relax> LockThen for Mutex<T, Rs, Rq> {}

#[cfg(test)]
impl<T: ?Sized, Rs: Relax, Rq: Relax> TryLockThen for Mutex<T, Rs, Rq> {}

#[cfg(test)]
mod test {
use crate::barging::lock_api::yields::Mutex;
Expand Down
3 changes: 3 additions & 0 deletions src/barging/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub use mutex::{Mutex, MutexGuard};
#[cfg_attr(docsrs, doc(cfg(feature = "lock_api")))]
pub mod lock_api;

#[cfg(feature = "thread_local")]
mod thread_local;

/// An unfair MCS lock that implements a `spin` relax policy.
///
/// During lock contention, this lock spins while signaling the processor that
Expand Down
Loading