Compare commits

...

3 commits

Author SHA1 Message Date
Jade Ellis
f36028b869
chore: Disable direnv's nix flake interfering with cargo cache
Some checks failed
Checks / Prefligit / prefligit (push) Failing after 1s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 5s
Checks / Rust / Clippy (push) Failing after 33s
Checks / Rust / Cargo Test (push) Failing after 31s
2025-07-19 23:32:53 +01:00
Jade Ellis
5b1d47fa4d
feat: Enable hardware-lock-elision and deadlock_detection 2025-07-19 23:32:18 +01:00
Jade Ellis
6e226dbd39
refactor: Allow with_lock to return data and take an async closure 2025-07-19 23:30:31 +01:00
4 changed files with 179 additions and 26 deletions

2
.envrc
View file

@ -2,6 +2,6 @@
dotenv_if_exists
use flake ".#${DIRENV_DEVSHELL:-default}"
# use flake ".#${DIRENV_DEVSHELL:-default}"
PATH_add bin

29
Cargo.lock generated
View file

@ -1659,6 +1659,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.1.2"
@ -3220,10 +3226,13 @@ version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"backtrace",
"cfg-if",
"libc",
"petgraph",
"redox_syscall",
"smallvec",
"thread-id",
"windows-targets 0.52.6",
]
@ -3273,6 +3282,16 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap 2.9.0",
]
[[package]]
name = "phf"
version = "0.11.3"
@ -4894,6 +4913,16 @@ dependencies = [
"syn",
]
[[package]]
name = "thread-id"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "thread_local"
version = "1.1.9"

View file

@ -517,6 +517,7 @@ version = "1.0"
[workspace.dependencies.parking_lot]
version = "0.12.4"
features = ["hardware-lock-elision", "deadlock_detection"] # TODO: Check if deadlock_detection has a perf impact, if it does only enable with debug_assertions
# Use this when extending with_lock::WithLock to parking_lot
[workspace.dependencies.lock_api]

View file

@ -1,89 +1,212 @@
//! Traits for explicitly scoping the lifetime of locks.
use std::sync::{Arc, Mutex};
use std::{
future::Future,
sync::{Arc, Mutex},
};
pub trait WithLock<T: ?Sized> {
/// Acquires a lock and executes the given closure with the locked data.
fn with_lock<F>(&self, f: F)
/// Acquires a lock and executes the given closure with the locked data,
/// returning the result.
fn with_lock<R, F>(&self, f: F) -> R
where
F: FnMut(&mut T);
F: FnMut(&mut T) -> R;
}
impl<T> WithLock<T> for Mutex<T> {
fn with_lock<F>(&self, mut f: F)
fn with_lock<R, F>(&self, mut f: F) -> R
where
F: FnMut(&mut T),
F: FnMut(&mut T) -> R,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock().unwrap();
f(&mut data_guard);
f(&mut data_guard)
// Lock is released here when `data_guard` goes out of scope.
}
}
impl<T> WithLock<T> for Arc<Mutex<T>> {
fn with_lock<F>(&self, mut f: F)
fn with_lock<R, F>(&self, mut f: F) -> R
where
F: FnMut(&mut T),
F: FnMut(&mut T) -> R,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock().unwrap();
f(&mut data_guard);
f(&mut data_guard)
// Lock is released here when `data_guard` goes out of scope.
}
}
impl<R: lock_api::RawMutex, T: ?Sized> WithLock<T> for lock_api::Mutex<R, T> {
fn with_lock<F>(&self, mut f: F)
fn with_lock<Ret, F>(&self, mut f: F) -> Ret
where
F: FnMut(&mut T),
F: FnMut(&mut T) -> Ret,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock();
f(&mut data_guard);
f(&mut data_guard)
// Lock is released here when `data_guard` goes out of scope.
}
}
impl<R: lock_api::RawMutex, T: ?Sized> WithLock<T> for Arc<lock_api::Mutex<R, T>> {
fn with_lock<F>(&self, mut f: F)
fn with_lock<Ret, F>(&self, mut f: F) -> Ret
where
F: FnMut(&mut T),
F: FnMut(&mut T) -> Ret,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock();
f(&mut data_guard);
f(&mut data_guard)
// Lock is released here when `data_guard` goes out of scope.
}
}
pub trait WithLockAsync<T> {
/// Acquires a lock and executes the given closure with the locked data.
fn with_lock<F>(&self, f: F) -> impl Future<Output = ()>
/// Acquires a lock and executes the given closure with the locked data,
/// returning the result.
fn with_lock<R, F>(&self, f: F) -> impl Future<Output = R>
where
F: FnMut(&mut T);
F: FnMut(&mut T) -> R;
/// Acquires a lock and executes the given async closure with the locked
/// data.
fn with_lock_async<R, F>(&self, f: F) -> impl std::future::Future<Output = R>
where
F: AsyncFnMut(&mut T) -> R;
}
impl<T> WithLockAsync<T> for futures::lock::Mutex<T> {
async fn with_lock<F>(&self, mut f: F)
async fn with_lock<R, F>(&self, mut f: F) -> R
where
F: FnMut(&mut T),
F: FnMut(&mut T) -> R,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock().await;
f(&mut data_guard);
f(&mut data_guard)
// Lock is released here when `data_guard` goes out of scope.
}
async fn with_lock_async<R, F>(&self, mut f: F) -> R
where
F: AsyncFnMut(&mut T) -> R,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock().await;
f(&mut data_guard).await
// Lock is released here when `data_guard` goes out of scope.
}
}
impl<T> WithLockAsync<T> for Arc<futures::lock::Mutex<T>> {
async fn with_lock<F>(&self, mut f: F)
async fn with_lock<R, F>(&self, mut f: F) -> R
where
F: FnMut(&mut T),
F: FnMut(&mut T) -> R,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock().await;
f(&mut data_guard);
f(&mut data_guard)
// Lock is released here when `data_guard` goes out of scope.
}
async fn with_lock_async<R, F>(&self, mut f: F) -> R
where
F: AsyncFnMut(&mut T) -> R,
{
// The locking and unlocking logic is hidden inside this function.
let mut data_guard = self.lock().await;
f(&mut data_guard).await
// Lock is released here when `data_guard` goes out of scope.
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_lock_return_value() {
let mutex = Mutex::new(5);
let result = mutex.with_lock(|v| {
*v += 1;
*v * 2
});
assert_eq!(result, 12);
let value = mutex.lock().unwrap();
assert_eq!(*value, 6);
}
#[test]
fn test_with_lock_unit_return() {
let mutex = Mutex::new(10);
mutex.with_lock(|v| {
*v += 2;
});
let value = mutex.lock().unwrap();
assert_eq!(*value, 12);
}
#[test]
fn test_with_lock_arc_mutex() {
let mutex = Arc::new(Mutex::new(1));
let result = mutex.with_lock(|v| {
*v *= 10;
*v
});
assert_eq!(result, 10);
assert_eq!(*mutex.lock().unwrap(), 10);
}
#[tokio::test]
async fn test_with_lock_async_return_value() {
use futures::lock::Mutex as AsyncMutex;
let mutex = AsyncMutex::new(7);
let result = mutex
.with_lock(|v| {
*v += 3;
*v * 2
})
.await;
assert_eq!(result, 20);
let value = mutex.lock().await;
assert_eq!(*value, 10);
}
#[tokio::test]
async fn test_with_lock_async_unit_return() {
use futures::lock::Mutex as AsyncMutex;
let mutex = AsyncMutex::new(100);
mutex
.with_lock(|v| {
*v -= 50;
})
.await;
let value = mutex.lock().await;
assert_eq!(*value, 50);
}
#[tokio::test]
async fn test_with_lock_async_closure() {
use futures::lock::Mutex as AsyncMutex;
let mutex = AsyncMutex::new(1);
mutex
.with_lock_async(async |v| {
*v += 9;
})
.await;
let value = mutex.lock().await;
assert_eq!(*value, 10);
}
#[tokio::test]
async fn test_with_lock_async_arc_mutex() {
use futures::lock::Mutex as AsyncMutex;
let mutex = Arc::new(AsyncMutex::new(2));
mutex
.with_lock_async(async |v: &mut i32| {
*v *= 5;
})
.await;
let value = mutex.lock().await;
assert_eq!(*value, 10);
}
}