Skip to content

Commit

Permalink
Add barging MCS lock (#4)
Browse files Browse the repository at this point in the history
* Add barging MCS lock

* Add lock_api feature
  • Loading branch information
pedromfedricci authored Dec 6, 2023
1 parent 81c4e1b commit bc62bbe
Show file tree
Hide file tree
Showing 18 changed files with 1,608 additions and 208 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ jobs:
run: rustup toolchain install stable
- name: Run raw example
run: cargo run --example raw
- name: Run barging example
run: cargo run --example barging
- name: Run thread_local example
run: cargo run --example thread_local --features thread_local
- name: Run lock_api example
run: cargo run --example lock_api --features lock_api

linter:
name: Linter
Expand All @@ -81,10 +85,12 @@ jobs:
run: cargo clippy --features yield
- name: Lint thread_local
run: cargo clippy --features thread_local
- name: Lint lock_api
run: cargo clippy --features lock_api
- name: Lint loom
env:
RUSTFLAGS: ${{ env.LOOM_RUSTFLAGS }}
run: cargo clippy --profile test --features thread_local
run: cargo clippy --profile test --all-features

miri:
name: Miri
Expand All @@ -98,7 +104,7 @@ jobs:
- name: Set Rust nightly as default
run: rustup default nightly
- name: Miri test
run: cargo miri test --features thread_local
run: cargo miri test --all-features

loom:
name: Loom
Expand All @@ -111,4 +117,4 @@ jobs:
- name: Loom test
env:
RUSTFLAGS: ${{ env.LOOM_RUSTFLAGS }}
run: cargo test --lib --release --features thread_local
run: cargo test --lib --release --all-features
21 changes: 19 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,31 @@ spin-lock for mutual exclusion, referred to as MCS lock.
name = "mcslock"
version = "0.1.0"
edition = "2021"
rust-version = "1.60.0"
# NOTE: Rust 1.65 is required for GATs and let-else statements.
rust-version = "1.65.0"
license = "MIT OR Apache-2.0"
readme = "README.md"
# documentation = "https://docs.rs/mcslock"
# homepage = "https://crates.io/mcslock"
repository = "https://github.com/pedromfedricci/mcslock"
authors = ["Pedro de Matos Fedricci <[email protected]>"]
categories = ["no-std", "no-std::no-alloc", "concurrency"]
categories = ["no-std", "concurrency"]
keywords = ["no_std", "mutex", "spin-lock", "mcs-lock"]

[workspace]
members = [".", "benches"]

[features]
# NOTE: Features `yield` and `thread_local` require std.
yield = []
thread_local = []
# NOTE: The `dep:` syntax requires Rust 1.60.
lock_api = ["dep:lock_api"]

[dependencies.lock_api]
version = "0.4"
default-features = false
optional = true

[target.'cfg(loom)'.dev-dependencies]
loom = { version = "0.7" }
Expand All @@ -30,6 +39,14 @@ loom = { version = "0.7" }
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[[test]]
name = "lock_api"
required-features = ["lock_api", "yield"]

[[example]]
name = "thread_local"
required-features = ["thread_local"]

[[example]]
name = "lock_api"
required-features = ["lock_api"]
13 changes: 8 additions & 5 deletions Makefile.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[env]
CFG_LOOM = "--cfg loom"

# Don't run these tasks for all crates in the workspace.
[config]
default_to_workspace = false
Expand Down Expand Up @@ -25,19 +28,19 @@ args = ["clippy", "--all-features", "--", "-D", "clippy::pedantic", "-D", "clipp
toolchain = "nightly"
install_crate = { rustup_component_name = "miri" }
command = "cargo"
args = ["miri", "test", "--features", "thread_local"]
args = ["miri", "test", "--all-features"]

# Run Loom tests.
[tasks.loom-test]
command = "cargo"
env = { "RUSTFLAGS" = "--cfg loom" }
args = ["test", "--lib", "--release", "--features", "thread_local"]
env = { "RUSTFLAGS" = "${CFG_LOOM}" }
args = ["test", "--lib", "--release", "--all-features"]

# Lint Loom cfg.
[tasks.loom-lint]
command = "cargo"
env = { "RUSTFLAGS" = "--cfg loom" }
args = ["clippy", "--profile", "test", "--features", "thread_local"]
env = { "RUSTFLAGS" = "${CFG_LOOM}" }
args = ["clippy", "--profile", "test", "--all-features", "--", "-D", "clippy::pedantic", "-D", "clippy::nursery"]

# Run busy loop bench.
[tasks.bench-busy]
Expand Down
130 changes: 78 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ mechanism are:
This algorithm and serveral others were introduced by [Mellor-Crummey and Scott] paper.
And a simpler correctness proof of the MCS lock was proposed by [Johnson and Harathi].

## Use cases

It is noteworthy to mention that [spinlocks are usually not what you want]. The
majority of use cases are well covered by OS-based mutexes like
[`std::sync::Mutex`] or [`parking_lot::Mutex`]. These implementations will notify
the system that the waiting thread should be parked, freeing the processor to
work on something else.

Spinlocks are only efficient in very few circunstances where the overhead
of context switching or process rescheduling are greater than busy waiting
for very short periods. Spinlocks can be useful inside operating-system kernels,
on embedded systems or even complement other locking designs. As a reference
use case, some [Linux kernel mutexes] run an customized MCS lock specifically
tailored for optimistic spinning during contention before actually sleeping.
This implementation is `no_std` by default, so it's useful in those environments.

## Install

Include the following under the `[dependencies]` section in your `Cargo.toml` file.
Expand All @@ -21,16 +37,53 @@ Include the following under the `[dependencies]` section in your `Cargo.toml` fi
# Cargo.toml

[dependencies]
# Avaliable features: `yield`, `thread_local`.
# Avaliable features: `yield`, `thread_local` and `lock_api`.
mcslock = { version = "0.1", git = "https://github.com/pedromfedricci/mcslock" }
```

## Raw locking APIs
## Documentation

Currently this project documentation is not hosted anywhere, you can render
the documentation by cloning this repository and then run:

```bash
cargo doc --all-features --open
```

## Barging MCS lock

This implementation will have non-waiting threads race for the lock against
the front of the waiting queue thread, which means this it is an unfair lock.
This implementation is suitable for `no_std` environments, and the locking
APIs are compatible with the [lock_api] crate. See `barging` and `lock_api`
modules for more information.

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

use mcslock::barging::spins::Mutex;

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

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

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

## Raw MCS lock

Raw locking APIs require exclusive access to a local queue node. This node is
represented by the `MutexNode` type. The `raw` module provides an implementation
that is `no_std` compatible, but also requires that queue nodes must be
instantiated by the callers.
This implementation operates under FIFO. Raw locking APIs require exclusive
access to a locally accessible queue node. This node is represented by the
`MutexNode` type. Callers are responsible for instantiating the queue nodes
themselves. This implementation is `no_std` compatible. See `raw` module for
more information.

```rust
use std::sync::Arc;
Expand All @@ -55,12 +108,14 @@ fn main() {
}
```

## Thread local locking APIs
## Thread local MCS lock

This crate also provides locking APIs that do not require user-side node
instantiation, by enabling the `thread_local` feature. These APIs require
that critical sections must be provided as closures, and are not compatible
with `no_std` environments as they require thread local storage.
This implementation also operates under FIFO. The locking APIs provided
by this module do not require user-side node allocation, critical
sections must be provided as closures and at most one lock can be held at
any time within a thread. It is not `no_std` compatible and can be enabled
through the `thread_local` feature. See `thread_local` module for more
information.

```rust
use std::sync::Arc;
Expand All @@ -74,57 +129,22 @@ fn main() {
let c_mutex = Arc::clone(&mutex);

thread::spawn(move || {
// Node instantiation is not required.
// Critical section must be defined as closure.
c_mutex.lock_with(|mut guard| *guard = 10);
})
.join().expect("thread::spawn failed");

// Node instantiation is not required.
// Critical section must be defined as closure.
assert_eq!(mutex.try_lock_with(|guard| *guard.unwrap()), 10);
}
```

## Documentation

Currently this project documentation is not hosted anywhere, you can render
the documentation by cloning this repository and then run:

```bash
cargo doc --all-features --open
```

## Use cases

[Spinlocks are usually not what you want]. The majority of use cases are well
covered by OS-based mutexes like [`std::sync::Mutex`] or [`parking_lot::Mutex`].
These implementations will notify the system that the waiting thread should
be parked, freeing the processor to work on something else.

Spinlocks are only efficient in very few circunstances where the overhead
of context switching or process rescheduling are greater than busy waiting
for very short periods. Spinlocks can be useful inside operating-system kernels,
on embedded systems or even complement other locking designs. As a reference
use case, some [Linux kernel mutexes] run an customized MCS lock specifically
tailored for optimistic spinning during contention before actually sleeping.
This implementation is `no_std` by default, so it's useful in those environments.

## API for `no_std` environments

The `raw` locking interface of a MCS lock is not quite the same as other
mutexes. To acquire a raw MCS lock, a queue node must be exclusively borrowed for
the lifetime of the guard returned by `lock` or `try_lock`. This node is exposed
as the `MutexNode` type. See their documentation for more information. If you
are looking for spin-based primitives that implement the [lock_api] interface
and also compatible with `no_std`, consider using [spin-rs].

## Features

This crate dos not provide any default features. Features that can be enabled
are:

### `yield`
### yield

The `yield` feature requires linking to the standard library, so it is not
suitable for `no_std` environments. By enabling the `yield` feature, instead
Expand All @@ -135,22 +155,28 @@ 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.

### `thread_local`
### thread_local

The `thread_local` feature provides locking APIs that do not require user-side
node instantiation, but critical sections must be provided as closures. This
node allocation, but critical sections must be provided as closures. This
implementation handles the queue's nodes transparently, by storing them in
the thread local storage of the waiting threads. These locking implementations
will panic if recursively acquired. Not `no_std` compatible.

### lock_api

This feature implements the [`RawMutex`] trait from the [lock_api] crate for
`barging::Mutex`. Aliases are provided by the `lock_api` module. This feature
is `no_std` compatible.

## Related projects

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

- `mcs-rs`: <https://github.com/gereeter/mcs-rs>
- `libmcs`: <https://github.com/topecongiro/libmcs>
- mcs-rs: <https://github.com/gereeter/mcs-rs>
- libmcs: <https://github.com/topecongiro/libmcs>

## License

Expand Down Expand Up @@ -180,7 +206,7 @@ each of your dependencies, including this one.
[spin-rs]: https://docs.rs/spin/latest/spin
[lock_api]: https://docs.rs/lock_api/latest/lock_api
[Linux kernel mutexes]: https://www.kernel.org/doc/html/latest/locking/mutex-design.html
[Spinlocks are usually not what you want]: https://matklad.github.io/2020/01/02/spinlocks-considered-harmful.html
[spinlocks are usually not what you want]: https://matklad.github.io/2020/01/02/spinlocks-considered-harmful.html
[Mellor-Crummey and Scott]: https://www.cs.rochester.edu/~scott/papers/1991_TOCS_synch.pdf
[Johnson and Harathi]: https://web.archive.org/web/20140411142823/http://www.cise.ufl.edu/tr/DOC/REP-1992-71.pdf
[cargo-crev]: https://github.com/crev-dev/cargo-crev
40 changes: 40 additions & 0 deletions examples/barging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread;

use mcslock::barging::spins::Mutex;

fn main() {
const N: usize = 10;

// Spawn a few threads to increment a shared variable (non-atomically), and
// let the main thread know once all increments are done.
//
// Here we're using an Arc to share memory among threads, and the data inside
// the Arc is protected with a mutex.
let data = Arc::new(Mutex::new(0));

let (tx, rx) = channel();
for _ in 0..N {
let (data, tx) = (data.clone(), tx.clone());
thread::spawn(move || {
// The shared state can only be accessed once the lock is held.
// Our non-atomic increment is safe because we're the only thread
// which can access the shared state when the lock is held.
//
// We unwrap() the return value to assert that we are not expecting
// threads to ever fail while holding the lock.
let mut data = data.lock();
*data += 1;
if *data == N {
tx.send(()).unwrap();
}
// the lock is unlocked here when `data` goes out of scope.
});
}
let _message = rx.recv();

let count = data.lock();
assert_eq!(*count, N);
// lock is unlock here when `count` goes out of scope.
}
Loading

0 comments on commit bc62bbe

Please sign in to comment.