Xutex is a family of high-performance synchronization primitives — Mutex, RwLock, Semaphore, Notify, Barrier and OnceCell — that seamlessly bridge synchronous and asynchronous Rust code with paired types sharing a unified internal representation. Designed for extremely low-latency acquisition under minimal contention, they achieve near-zero overhead on the fast path while remaining runtime-agnostic.
- ⚡ Blazing-fast async performance: Up to 50× faster than standard sync mutexes in single-threaded async runtimes, and 2–6× faster than
tokio::syncprimitives under contention on multi-threaded runtimes. - 🧰 Complete primitive family:
Mutex,RwLock,Semaphore,Notify,BarrierandOnceCell— every one available in a sync and an async flavor sharing the same algorithm - 🔄 Hybrid API: Every primitive comes as a layout-identical
X/AsyncXpair with zero-cost conversions (as_async(),to_sync(),Arccasts), so the same object serves blocking threads and async tasks - ⚡ 8-byte lock state: Single
AtomicPtrmutex state on 64-bit platforms (guarded data stored separately) - 🚀 Zero-allocation fast path: Acquisition requires no heap allocation when uncontended; waiters are stack-allocated intrusive nodes, so even contended waits allocate nothing per waiter
- ♻️ Smart allocation reuse: The only heap object — the wait queue itself — is pooled and exists only while waiters are parked
- 🎯 Runtime-agnostic: Works with Tokio, async-std, monoio, or any executor using
std::task::Waker - 🔒 Lock-free fast path: Single CAS operation for uncontended acquisition
- ⚖️ Fair by construction: Semaphore and RwLock are strict-FIFO (write-preferring RwLock, like tokio); no starvation
- 🛑 Cancellation-safe futures: Dropping any pending acquisition future cleanly unregisters it (and returns already-granted permits); barrier waits even withdraw their arrival
- 🧪 Model-checked & UB-checked: Exhaustively tested with loom and miri
- 📦 Minimal footprint: Compact state representation with lazy queue allocation
- 🛡️ No-std compatible: The async primitives are fully
no_std(onlycore+alloc)
[dependencies]
xutex = "0.2"Or via cargo:
# with std
cargo add xutex
# for no-std environments
cargo add xutex --no-default-features#[cfg(feature = "std")]
fn example() {
use xutex::Mutex;
let mutex = Mutex::new(0);
{
let mut guard = mutex.lock();
*guard += 1;
} // automatically unlocked on drop
assert_eq!(*mutex.lock(), 1);
}use xutex::AsyncMutex;
async fn increment(mutex: &AsyncMutex<i32>) {
let mut guard = mutex.lock().await;
*guard += 1;
}Convert seamlessly between sync and async:
#[cfg(feature = "std")]
use xutex::Mutex;
#[cfg(feature = "std")]
async fn example(mutex: &Mutex<i32>) {
let async_ref = mutex.as_async();
let guard = async_ref.lock().await;
}#[cfg(feature = "std")]
fn example(){
use xutex::{Mutex, AsyncMutex};
// Async → Sync
let async_mutex = AsyncMutex::new(5);
let sync_ref: &Mutex<_> = async_mutex.as_sync();
let guard = sync_ref.lock();
drop(guard);
// Block on async mutex from sync context
let guard = async_mutex.lock_sync();
}Every primitive follows the same split: a synchronous type (RwLock, Semaphore, Notify, Barrier, OnceCell) and a layout-identical asynchronous twin (AsyncRwLock, AsyncSemaphore, AsyncNotify, AsyncBarrier, AsyncOnceCell), freely convertible in both directions:
#[cfg(feature = "std")]
fn example() {
use xutex::{RwLock, Semaphore, Notify, Barrier, OnceCell};
// Reader-writer lock: many readers or one writer, write-preferring FIFO.
let lock = RwLock::new(0);
{
let r1 = lock.read();
let r2 = lock.read();
assert_eq!(*r1 + *r2, 0);
}
*lock.write() += 1;
// Counting semaphore with atomic multi-permit acquisition.
let sem = Semaphore::new(4);
let permit = sem.acquire_many(2).unwrap();
assert_eq!(sem.available_permits(), 2);
drop(permit);
// Notification: store-one-permit or wake-all.
let notify = Notify::new();
notify.notify_one();
notify.wait(); // consumes the stored permit without blocking
// Reusable barrier.
let barrier = Barrier::new(1);
assert!(barrier.wait().is_leader());
// Initialize-once cell with waiter takeover on failure.
let cell = OnceCell::new();
assert_eq!(*cell.get_or_init(|| 42), 42);
}use xutex::{AsyncRwLock, AsyncSemaphore};
async fn example(lock: &AsyncRwLock<i32>, sem: &AsyncSemaphore) {
let value = *lock.read().await;
let _permit = sem.acquire().await.unwrap();
let mut w = lock.write().await;
*w += value;
// Dropping any pending future (e.g. inside tokio::select!) is safe:
// it unregisters from the wait queue and returns any granted permits.
}-
Atomic state machine: Three states encoded in a single pointer:
UNLOCKED(null): Lock is freeLOCKED(sentinel): Lock held, no waitersUPDATING: Queue initialization in progressQUEUE_PTR: Lock held with waiting tasks/threads
-
Lock-free fast path: Uncontended acquisition uses a single
compare_exchange -
Lazy queue allocation: Wait queue created only when contention occurs
-
Pointer tagging: LSB tagging prevents race conditions during queue modifications
-
Stack-allocated waiters:
Signalnodes live on the stack, forming an intrusive linked list -
Optimized memory ordering: Careful use of
Acquire/Releasesemantics -
Adaptive backoff: Exponential backoff reduces cache thrashing under contention
-
Minimal heap allocation: At most one allocation per contended lock via pooled queue reuse, additional waiters require zero allocations
Run benchmarks on your machine:
cargo benchExpected Performance (varies by hardware):
- Uncontended: ~1-3ns per lock/unlock cycle (single CAS operation)
- High contention: 2-3× faster than
tokio::sync::Mutexin async contexts - Sync contexts: Performance comparable to
std::sync::Mutexwith minimal overhead from queue pointer checks under high contention; matchesparking_lotperformance in low-contention scenarios
Sample results for the newer primitives (Linux x86-64, 16 logical cores; cargo bench):
| Benchmark | xutex | tokio | std | parking_lot |
|---|---|---|---|---|
RwLock read, uncontended |
6.4 ns | 28.9 ns | 5.7 ns | 6.3 ns |
RwLock write, uncontended |
6.6 ns | — | 5.7 ns | 5.6 ns |
RwLock contended, tokio rt (64 tasks, 7:1 r/w) |
449 µs | 872 µs | — | — |
Semaphore acquire/release, uncontended |
6.3 ns | 28.3 ns | — | — |
Semaphore contended, tokio rt (64 tasks, 4 permits) |
277 µs | 1.67 ms | — | — |
Notify permit round-trip |
9.3 ns | 12.9 ns | — | — |
┌─────────────────────────────────────────────┐
│ Mutex<T> / AsyncMutex<T> │
│ ┌───────────────────────────────────────┐ │
│ │ MutexInternal<T> │ │
│ │ • queue: AtomicPtr<QueueStructure> │ │
│ │ • inner: UnsafeCell<T> │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
│
├─ UNLOCKED (null) ──────────────► Lock available
│
├─ LOCKED (sentinel) ─────────────► Lock held, no waiters
│
└─ Queue pointer ─────────────────► Lock held, waiters queued
│
▼
┌─────────────────┐
│ SignalQueue │
│ (linked list) │
└─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ Signal │────►│ Signal │────► ...
│ • waker │ │ • waker │
│ • value │ │ • value │
└─────────────────┘ └─────────────────┘
Each waiter tracks its state through atomic transitions:
SIGNAL_UNINIT (0): Initial stateSIGNAL_INIT_WAITING (1): Enqueued and waitingSIGNAL_SIGNALED (2): Lock grantedSIGNAL_RETURNED (!0): Guard has been returned
- Public API: 100% safe Rust
- Internal implementation: Carefully controlled
unsafeblocks for:- Queue manipulation (pointer tagging prevents use-after-free)
- Guard creation (guaranteed by state machine)
- Memory ordering (documented and audited)
| Method | Description |
|---|---|
new(data: T) |
Create a new synchronous mutex |
lock() |
Acquire the lock (blocks current thread) |
try_lock() |
Attempt non-blocking acquisition |
lock_async() |
Acquire asynchronously (returns Future) |
as_async() |
View as &AsyncMutex<T> |
to_async() |
Convert to AsyncMutex<T> |
to_async_arc() |
Convert Arc<Mutex<T>> to Arc<AsyncMutex<T>> |
| Method | Description |
|---|---|
new(data: T) |
Create a new asynchronous mutex |
lock() |
Acquire the lock (returns Future) |
try_lock() |
Attempt non-blocking acquisition |
lock_sync() |
Acquire synchronously (blocks current thread) |
as_sync() |
View as &Mutex<T> |
to_sync() |
Convert to Mutex<T> |
to_sync_arc() |
Convert Arc<AsyncMutex<T>> to Arc<Mutex<T>> |
Implements Deref<Target = T> and DerefMut for transparent access to the protected data. Automatically releases the lock on drop.
| Method | Description |
|---|---|
new(data) / with_max_readers |
Create (default max readers: u32::MAX >> 3) |
read() / write() |
Acquire shared/exclusive access (blocking or future) |
try_read() / try_write() |
Non-blocking attempts |
read_async() … / read_sync() … |
Cross-mode acquisition |
RwLockWriteGuard::downgrade() |
Atomically demote a write guard to a read guard |
get_mut() / into_inner() |
Direct access through exclusive ownership |
Fair and write-preferring (strict FIFO, tokio-style): queued writers block later readers, so writers cannot starve.
| Method | Description |
|---|---|
new(permits) |
Create with an initial permit count |
acquire() / acquire_many(n) |
Acquire permits (blocking or future), FIFO |
try_acquire() / try_acquire_many |
Non-blocking attempts |
add_permits(n) / forget_permits |
Grow / shrink the pool |
close() / is_closed() |
Fail all current and future acquisitions |
SemaphorePermit::{forget,split,merge} |
Permit manipulation |
acquire_many hands over all n permits atomically; a partially served waiter blocks later waiters (fairness), and cancellation returns partially assigned permits.
notify_one() (wakes one waiter or stores a single permit), notify_waiters() (wakes all current waiters; also completes every notified() future created before the call), wait()/notified(). A notify_one wakeup consumed by a dropped future is passed on to the next waiter, matching tokio semantics.
new(n), wait() → BarrierWaitResult::is_leader(). Reusable across rounds. Unlike tokio::sync::Barrier, dropping a pending wait future withdraws the arrival, so cancellation cannot deadlock the barrier.
get, set, get_or_init, get_or_try_init, initialized, get_mut, take, into_inner. Concurrent initializers race; losers park allocation-free. A failed, panicking, or cancelled initializer hands the role to a queued waiter (which runs its own closure), matching tokio semantics.
- High-frequency, low-contention async locks
- Hybrid applications mixing sync and async code
- Performance-critical sections with short critical regions
- Runtime-agnostic async libraries
- Situations requiring zero-allocation fast paths
- Predominantly synchronous workloads: In pure sync environments without async interaction,
std::syncprimitives may offer slightly better performance due to lower abstraction overhead - Mutex poison state: Cases where
std::sync::Mutexpoisoning semantics are required
- 8-byte claim: Refers to lock metadata only on 64-bit platforms; guarded data
Tstored separately - No poisoning: Unlike
std::sync::Mutex, panics don't poison the lock - Sync overhead: Slight performance cost vs
std::sync::Mutexin pure-sync scenarios (~1-5%)
Run the test suite:
# Standard tests
cargo test
# With Miri (undefined behavior detection)
cargo +nightly miri test
# With loom (exhaustive concurrency model checking)
RUSTFLAGS="--cfg loom" cargo test --release --test loom
# Benchmarks
cargo benchThe loom suite explores every thread interleaving (including weak-memory
reorderings) of small scenarios for each primitive: lock handoff, permit
handoff and partial acquire_many assignment, close/cancel races, lost-wakeup
races in Notify, barrier rounds and once-cell initializer races. Under
loom, the internal wait-queue spinlock and waker slot are modeled with
loom-aware locks (loom cannot prove progress through raw spin loops); the
spinlock mechanics themselves are covered by miri and the native stress
tests.
- Implement
RwLockvariant with shared/exclusive locking -
Semaphore,Notify,BarrierandOnceCellprimitives - Loom-based exhaustive concurrency testing
- Owned (
Arc-based) guard variants (OwnedSemaphorePermit, …) - Explore lock-free linked list implementation for improved wait queue performance
Contributions are welcome! Please:
- Run
cargo +nightly fmtandcargo clippybefore submitting - Add tests for new functionality
- Update documentation as needed
- Verify
cargo miri testpasses - Note: This library is
no-stdcompatible; usecoreandallocinstead ofstd. Ensurecargo testandcargo test --no-default-featuresrun without warnings.
Licensed under the MIT License.
Author: Khashayar Fereidani
Repository: github.com/fereidani/xutex