Expand description
§BorrowMutex
BorrowMutex is an async Mutex which does not require wrapping the target
structure. Instead, a &mut T can be lended to the mutex at any time.
This lets any other side borrow the &mut T. The mutable ref is borrow-able
only while the lender awaits, and the lending side can await until someone
wants to borrow. The semantics enforce at most one side has a mutable reference
at any given time.
This lets us share any mutable object between distinct async contexts
without Arc<Mutex> over the object in question and without relying
on any kind of internal mutability.
The most common use case is having a state handled entirely in its own async context, but occasionally having to be accessed from the outside - another async context.
Since the shared data doesn’t have to be wrapped inside an Arc,
it doesn’t have to be allocated on the heap. In fact, BorrowMutex does not
perform any allocations whatsoever. The
tests/borrow_basic.rs
presents a simple example where everything is stored on the stack.
§Example
let mutex = BorrowMutex::<16, i32>::new();
let lender = async {
let mut value = 0;
mutex.wait_to_lend().await;
mutex.lend(&mut value).unwrap().await;
};
let borrower = async {
*mutex.try_borrow().await.unwrap() += 1;
};A more realistic example would be to futures::select! on the lender side on mutex.wait_to_lend() and some conn.incoming_message().
§Benchmarks
Measured locally with:
cargo bench --bench mutex -- --sample-size 10 --warm-up-time 1 --measurement-time 1Criterion median time per batch; lower is better. Each user performs 1,000
operations per batch. The uncontended BorrowMutex workload polls
wait_to_lend().now_or_never() without a borrower; the other mutexes lock and
update a value. The APIs and synchronization mechanisms are not identical, so
compare the times with that distinction in mind.
| Workload | BorrowMutex | smol::lock::Mutex | tokio::sync::Mutex |
|---|---|---|---|
| Uncontended (1 user) | 5.329 µs | 6.091 µs | 21.683 µs |
| Contended (2 users) | 217.25 µs | 197.68 µs | 219.33 µs |
| Contended (8 users) | 1.2501 ms | 1.1821 ms | 0.90964 ms |
| Contended (32 users) | 5.3907 ms | 4.7255 ms | 3.5714 ms |
Uncontended time per 1,000 operations (one █ ≈ 1 µs; shorter is faster):
BorrowMutex █████ 5.33 µs
smol::lock::Mutex ██████ 6.09 µs
tokio::sync::Mutex ██████████████████████ 21.68 µsThe barrier and repeated-lock workload is adapted from the methodology of ytakano/async_bench. The contention interpretation was also informed by khonsulabs/async-locking-benchmarks.
Both futures should print interchangeably. See tests/borrow_basic.rs for
a full working example.
§Safety
Calling std::mem::forget() on a LendGuard may lead to undefined behavior.
Dropping it while the value is borrowed is protected by aborting the process.
See BorrowMutex::lend for the full contract.
Structs§
- Borrow
Guard Armed - RAII scoped lock guard. This is an armed variant which provides access
to the lended value via its
DerefandDerefMutimplementations. - Borrow
Guard Unarmed - RAII scoped lock guard. This is an unarmed variant which is still waiting for a value to be lended.
- Borrow
Mutex - Async Mutex which does not require wrapping the target structure.
A
&mut Tcan be lended to the mutex at any given time. - Lend
Guard - RAII scoped lock guard for the lending side. This is created by
BorrowMutex::lend(), and has to be polled until the associated borrower drops. - Lend
Waiter - A Future returned by
BorrowMutex::wait_to_lend()which resolves as soon as any borrow request is pending.
Enums§
- Error
- Possible borrow errors