Crates.io | interlock |
lib.rs | interlock |
version | 0.0.4 |
source | src |
created_at | 2021-09-05 15:56:45.844863 |
updated_at | 2023-05-06 05:50:12.962959 |
description | Readers-writer locks designed for locking intervals |
homepage | |
repository | https://github.com/yvt/interlock-rs |
max_upload_size | |
id | 447184 |
size | 244,266 |
Experimental, requires unstable compiler features
Readers-writer locks designed for locking intervals. #![no_std]
compatible.
use std::pin::Pin;
use interlock::{hl::slice::SyncRbTreeVecIntervalRwLock, state};
let vec = Box::pin(SyncRbTreeVecIntervalRwLock::new(vec![0u8; 64]));
let vec = vec.as_ref();
// Borrow `vec[0..32]`
state!(let mut state);
let guard1 = vec.read(0..32, (), state);
// Borrow `vec[16..32]`
state!(let mut state);
let guard2 = vec.read(16..32, (), state);
// Mutably borrow `vec[16..48]` unsuccessfully
state!(let mut state);
vec.try_write(16..48, Pin::as_mut(&mut state)).unwrap_err();
// Unborrow `vec[0..32]` completely
drop(guard1);
drop(guard2);
// Mutably borrow `vec[16..48]`
vec.try_write(16..48, Pin::as_mut(&mut state)).unwrap();
use parking_lot::RawMutex;
use interlock::{hl::slice::AsyncRbTreeVecIntervalRwLock, state};
#[tokio::main]
async fn main() {
let vec = Box::pin(AsyncRbTreeVecIntervalRwLock::<RawMutex, _>::new(vec![0u8; 64]));
let vec = vec.as_ref();
state!(let mut state);
let _guard = vec.async_read(0..32, (), state).await;
state!(let mut state);
let _guard = vec.async_read(16..48, (), state).await;
}
Locking Interface | Storage | Type Alias |
---|---|---|
Fallible + Panicking, !Sync |
&mut [T] |
[crate::hl::slice::LocalRbTreeSliceRefIntervalRwLock ] |
↑ | Vec<T> |
[crate::hl::slice::LocalRbTreeVecIntervalRwLock ] |
Fallible + Blocking | &mut [T] |
[crate::hl::slice::SyncRbTreeSliceRefIntervalRwLock ] |
↑ | Vec<T> |
[crate::hl::slice::SyncRbTreeVecIntervalRwLock ] |
Fallible + Future -oriented |
&mut [T] |
[crate::hl::slice::AsyncRbTreeSliceRefIntervalRwLock ] |
↑ | Vec<T> |
[crate::hl::slice::AsyncRbTreeVecIntervalRwLock ] |
They are modeled as an array of virtual readers-writer locks. When locking, the virtual locks in the specified range are locked in ascending order. When unlocking, they are unlocked in descending order. (The locking order is important to prevent deadlocks.) The wait list of each virtual lock is ordered by (priority, sequence)
, where sequence
is a monotonically increasing number (thus enforcing FIFO ordering). When a virtual lock is unlocked, all entries in the wait list are examined and resumed instantly (if possible) in order.
On a lock conflict, the fallible interface fails and leaves all virtual locks unchanged. Other interfaces maintain the incomplete borrow until it's cancelled or completed by the removal of a conflicting borrow.
The Future
-oriented interface supports cancellation.
The worst-case time complexity is shown below:
Operation | Time Complexity |
---|---|
Locking | O(log(existing_borrows)) |
Unlocking | O((1 + potentially_resumed_borrows) * log(existing_borrows)) |
The space complexity is O(existing_borrows)
.
std
enables the items that depend on std
or alloc
.alloc
enables the items that depend on alloc
. This currently requires a target with usize
atomics support because stable_deref_trait/alloc
unconditionally implements StableDeref
on Arc
, which is gated by cfg(target_has_atomic = "ptr")
.async
enables the Future
-oriented API. This currently requires a target with load/store atomics support. When lock_api
issue #277 is resolved, this requirement will be lifted, and this Cargo feature will be deprecated.impl
s and various kinds of safety.Mutex
and ReentrantMutex
counterparts should be provided in addition to that of RwLock
.EarlyDropGuard
) should be removed when Rust's memory model is adjusted to properly accommodate such structures.interlock
is surprisingly niche. While it may scale well for large-scale inputs, its complexity and overhead are likely to outweigh the benefit in many practical situations. Consider the following alternatives before choosing interlock
:
Dividing slices: The language and the standard library provide many ways to divide &mut [T]
. For example:
if let [first, rest @ ..] ...
) can separate the first or last few elements from the rest of a given slice.slice::iter_mut
] creates a set of &mut T
, which can be used separately.slice::chunks_mut
] breaks &mut [T]
into chunks.slice::split_at_mut
] divides &mut [T]
into two. An arbitrary number of pieces can be created by repeating this process.No ordering requirements, no reference forming: If borrows can be unordered in respect to other borrows, getting access to &[T]
or &mut [T]
is not necessary, and...
[T]
is only referenced by a single thread, then consider wrapping individual elements with Cell
<T>
. You can turn &mut [T]
into &[Cell<T>]
by Cell::as_slice_of_cells
.std::sync::atomic
]::Atomic*
and accessing them with Ordering::Relaxed
.Short-lived borrows: If each borrow is expected to be short-lived, consider wrapping the whole slice with a Mutex
or RwLock
. interlock
uses a Mutex
to protect internal structures, so it incurs the overhead of Mutex
plus bookkeeping.
Parallel data processing: Consider using Rayon for parallel computation.
Per-element or per-segment borrows: If you only need to borrow individual elements or segments (i.e., if it's acceptable to get &mut T
or &mut [T; SEGMENT_LEN]
instead of a more general, contiguous &mut [T]
), consider wrapping individual elements or segments with parking_lot
::{Mutex, RwLock}
, which only take one byte and one word of space, respectively. In some cases, spin
might be a more preferable choice.
N
borrows from Mutex
with one borrow from SyncRbTreeSliceIntervalRwLock
. While the latter's locking time doesn't depend on N
, due its inherent complexity, if N
is too small, you might actually lose performance. According to some single-threaded micro-benchmark conducted on an AMD Zen processor, the threshold seems to be somewhere around N = 6
. Expect a higher threshold in a real application because of cache effects and lock contention.Single processor, CPU-bound: If only one processor accesses the slice simultaneously, and it's expected that the processor is fully occupied while the slice is borrowed, consider wrapping the whole slice with a Mutex
or RwLock
. In such cases, being able to borrow disjoint subslices simultaneously doesn't improve the overall throughput.