The Resync Guidebook
Welcome to the guide for Resync. This document covers the library’s philosophy, core concepts, advanced usage patterns, design decisions, and inherent limitations.
Resync is not just another mutex crate. It is a LEGO-like toolkit for synchronization. Whether you are building a high-throughput user-space server, a bare-metal embedded kernel, or a complex distributed system, Resync gives you the raw materials to build exactly the synchronization primitive you need, while enforcing safe, race-free API boundaries.
In this book, you will learn how to decouple acquisition from waiting, how
to prevent deadlocks at compile time, and how to leverage Resync’s impressive
suite of built-in batteries like Gate, Semaphore, and Shield.
1. Philosophy: The “LEGO” Approach
Unlike standard library synchronization primitives that provide a monolithic
Mutex or RwLock, Resync treats synchronization as a composition of
smaller, independent behaviors.
A blocking mutex is essentially two behaviors combined:
- Acquisition: How do we atomically claim ownership of a resource?
- Waiting: What do we do while the resource is held by someone else?
Resync decouples these concerns into the LockPolicy and
RetryPolicy traits. This allows you to mix and match atomic acquisition
strategies with different spin-wait strategies at compile time, tailoring
the primitive exactly to your performance and environment constraints.
2. Core Traits
LockPolicy: The Acquisition Strategy
The LockPolicy trait defines the raw mechanics of claiming a resource.
It is intentionally minimal and strictly atomic. It includes an associated
Meta type, allowing implementations to pass state (like a ticket number or
guard token) from the acquisition step (try_lock) to the release step
(free).
RetryPolicy: The Waiting Strategy
The RetryPolicy trait defines what the CPU should do when a LockPolicy
reports contention. This could be a tight CPU pause loop (retry::Busy),
yielding to the OS scheduler (retry::Yield), or even a custom exponential
backoff strategy.
SharingPolicy: The Read-Write Semantics
Extends LockPolicy to support shared (reader) access. This allows a single
lock primitive to support both reader and writer access, forming the basis for
the Sharex (RwLock) primitive.
NewLocked: The Initialization Strategy
Allows locks to be created in an already-acquired (locked) state. This is
crucial for primitives like Gate that must start closed to prevent
Time-of-Check to Time-of-Use (TOCTOU) races. By segregating this into a
separate trait, Resync follows the Interface Segregation Principle.
Mutex & Sharex: The Composition
These structs bind a LockPolicy, a RetryPolicy, and the protected data (T)
together, providing a safe, RAII-based interface (ExGuard and ShGuard).
They also manage the Lock Poisoning state, automatically detecting thread
panics (when std is enabled) to protect data integrity.
3. Batteries Included
While the core philosophy of Resync is modularity, it doesn’t mean you have to build everything from scratch. Resync ships with an impressive, production-ready arsenal of synchronization primitives and backend policies out of the box.
High-Level Primitives
Mutex<T, L, R>: The standard mutual exclusion lock. Protects data and supports lock poisoning.Sharex<T, L, R>: A read-write lock (RwLock) allowing multiple concurrent readers or a single exclusive writer.Gate<L, R>: A controllable barrier. Starts closed (viaNewLocked) and blocks threads until explicitly opened. Perfect for thread pool initialization.Semaphore<L, R>: A counting semaphore for limiting concurrent access to a pool of resources (e.g., DB connections).Condvar: A condition variable for event-based waiting, fully respecting the poisoning semantics of the associatedMutex.Barrier<R>: A synchronization primitive that blocks a set of threads until all of them have reached a certain point.
Lock Backends (LockPolicy & SharingPolicy)
Atomic: A pure, portable spinlock based onAtomicUsize. Ideal for#![no_std]and extremely short critical sections.Os: OS-specific high-performance locks. Usesfutexon Linux,pthread_rwlock_ton macOS, andSRWLOCKon Windows. Automatically parks threads in the kernel on contention.Fs: A filesystem-based advisory lock usingflock(2). Useful for cross-process synchronization.Irq: A bare-metal lock that disables hardware interrupts (IRQs) upon acquisition. Essential for kernel development to prevent interrupt-handler deadlocks.Nested<L1, L2>: A composite lock that strictly enforces acquisition order (L1thenL2) and reverse release order, preventing deadlocks at compile time.Shield<L>: A wrapper that prevents writer starvation in read-write locks by yielding readers (shared accessors yield) the resource from new readers when a writer is waiting.
Retry Backends (RetryPolicy)
Busy: Executes architecture-specific CPU pause instructions (core::hint::spin_loop()).Yield: Cooperatively yields the current thread to the OS scheduler (std::thread::yield_now()).
4. Comparisons with the Ecosystem
How does Resync stack up against the standard library and popular third-party crates?
std::sync vs resync
The standard library provides Mutex and RwLock, but they are monolithic.
You cannot change how they wait (they always yield/park via the OS), and they
are strictly tied to std. Furthermore, std::sync::Mutex::lock() returns a
PoisonError, which conflates poisoning with other potential lock failures.
Resync separates these concerns using AcquireError, distinguishing between
Poisoned (data inconsistency), Lock (fatal OS/hardware error), and
Retry (timeout).
parking_lot vs resync
parking_lot is incredibly fast and widely used. However, it hardcodes its
OS-level parking mechanism. If you are writing a hybrid application where 99%
of locks should park in the OS, but 1% of locks (e.g., inside a specific audio
processing callback or embedded context) must never yield to the OS,
parking_lot forces you to use a completely different crate (like spin) for
that 1%. Resync allows you to use the exact same Mutex<T> API, simply
swapping the generic parameter from Os to Atomic and Yield to Busy for
that specific critical section.
spin vs resync
The spin crate provides excellent pure spinlocks, but lacks advanced
composable features like SharingPolicy (RW semantics), NewLocked
(TOCTOU-free initialization), and adaptive retry strategies out of the box.
Resync’s Atomic backend provides similar raw performance, but plugs into a
much richer ecosystem of high-level primitives like Gate and Semaphore.
lock_api vs resync::api
lock_api is the standard for abstracting over locks in the ecosystem. However,
its RawMutex trait assumes that locking is infallible (it cannot return an
error, nor can it represent timeouts or poisoning). Resync provides its own
resync::api::Mutex trait, which embraces granular error handling. It allows
generic code to accept any compatible synchronization primitive while
preserving safety guarantees like poisoning and timeout handling.
5. Usage Cases & Full Examples
Case A: The Fair Read-Write Lock (Preventing Writer Starvation)
In standard std::sync::RwLock or basic atomic RW locks, a continuous stream
of readers can starve a waiting writer indefinitely. Resync solves this with
the Shield battery, which wraps any SharingPolicy and blocks new readers
the moment a writer starts waiting.
#![allow(unused)]
fn main() {
use resync::{Sharex, lock::{Os, Shield}, retry::Yield};
// Define a fair RW lock type
type FairRwLock<T> = Sharex<T, Shield<Os>, Yield>;
let lock = FairRwLock::new(vec![1, 2, 3]);
// Readers can proceed concurrently
let r1 = lock.read().unwrap();
// If a writer tries to acquire and fails, Shield increments a pending
// counter. Subsequent readers will receive `LockStatus::Fail` and yield,
// guaranteeing the writer gets the lock as soon as `r1` is dropped.
}
Case B: Thread Pool Initialization with Gate
When spawning a pool of worker threads, you often want them to block until the
main thread finishes setting up the environment. Using a std::sync::Barrier
requires knowing the exact number of threads upfront and is single-use. Using
channels introduces allocation overhead.
Gate starts closed (via the NewLocked trait), ensuring no thread can slip
through before the setup is done (preventing TOCTOU races).
use resync::{Gate, lock::Os, retry::Yield};
use std::sync::Arc;
use std::thread;
fn main() {
// Gate is CLOSED by default.
let gate = Arc::new(Gate::<Os, Yield>::new());
let workers: Vec<_> = (0..8).map(|id| {
let g = Arc::clone(&gate);
thread::spawn(move || {
// All 8 threads block here immediately.
g.wait().unwrap();
println!("Worker {id} is processing!");
})
}).collect();
// Main thread does heavy setup...
std::thread::sleep(std::time::Duration::from_secs(1));
// Unleash all workers simultaneously.
gate.open();
for w in workers { w.join().unwrap(); }
}
Case C: Bare-Metal Kernel Development (no_std + Irq)
In OS kernel development, if a thread holds a spinlock and gets interrupted by a hardware IRQ, and the IRQ handler tries to acquire the same spinlock, the system deadlocks.
Resync provides the Irq lock policy, which automatically saves the CPU flags,
disables interrupts on acquisition, and restores them on release.
// In a #![no_std] kernel environment
use resync::{Mutex, lock::Irq, retry::Busy};
// The lock protects per-CPU data.
static KERNEL_STATE: Mutex<u32, Irq, Busy> = Mutex::new(0);
fn thread_context() {
// Interrupts are disabled while this guard is alive.
let mut state = KERNEL_STATE.lock().unwrap();
*state += 1;
}
fn hardware_interrupt_handler() {
// Safe to acquire the lock here, because thread_context
// guaranteed interrupts were disabled before taking it.
let mut state = KERNEL_STATE.lock().unwrap();
*state += 1;
}
Case D: Graceful Recovery from Poisoning
Unlike std::sync::Mutex which forces you to unwrap() or manually extract
the inner error, Resync’s AcquireError allows you to match on the exact
reason of failure, including timeouts from custom RetryPolicy
implementations.
#![allow(unused)]
fn main() {
use resync::{Mutex, AcquireError, lock::Os, retry::Yield};
let mutex = Mutex::<i32>::new(42);
match mutex.lock() {
Ok(guard) => println!("Data is safe: {}", *guard),
Err(AcquireError::Poisoned(err)) => {
println!("Thread panicked! Inspecting corrupted data...");
let mut guard = err.into_inner();
*guard = 0; // Manually repair the state
unsafe { mutex.clear_poison(); }
},
Err(AcquireError::Retry(timeout_err)) => {
eprintln!("Lock acquisition timed out: {}", timeout_err);
},
Err(AcquireError::Lock(os_err)) => {
eprintln!("Fatal OS error: {}", os_err);
}
}
}
3. Design Decisions
Why no is_locked() or is_free() in LockPolicy?
A common question when designing lock APIs is: “Why can’t I check if the lock is currently held before trying to acquire it?”
In concurrent programming, checking a state and then acting on it introduces a Time-of-Check to Time-of-Use (TOCTOU) race condition. Consider this hypothetical anti-pattern:
#![allow(unused)]
fn main() {
// HYPOTHETICAL BAD CODE
if !lock.is_locked() {
// Another thread could acquire the lock RIGHT HERE
lock.lock();
}
}
By the time you call lock() after checking is_locked(), the state may have already changed. The check is not only wasted CPU cycles, but it gives a false sense of security and predictability.
By omitting state-querying methods, Resync forces you to use
LockPolicy::try_lock, which is an atomic check-and-acquire
operation. The result of the operation tells you the state at the exact
moment the atomic instruction executed, eliminating TOCTOU bugs by design.
Why AcquireError and TryLockError instead of just PoisonError?
Standard library locks return PoisonError<Guard>, which conflates poisoning
with other potential lock failures. Resync separates these concerns using
AcquireError and TryLockError. These enums distinguish between:
- Poisoned: A previous thread panicked, and the data might be inconsistent.
- Lock: A fatal, unrecoverable error occurred in the underlying
LockPolicy(e.g., OS resource exhaustion). - Retry: The
RetryPolicyaborted the wait loop (e.g., due to a timeout).
This granular error handling allows no_std environments and complex systems to
react appropriately to timeouts or hardware failures without relying on panics.
Why is Lock Poisoning std-only?
Lock poisoning relies on detecting whether the current thread is unwinding due to
a panic (std::thread::panicking()). In #![no_std] environments (like kernels or
embedded systems), a panic typically triggers an immediate abort or system reset,
making the concept of “recovering from a panic inside a lock” inapplicable. Therefore,
the poisoning machinery (AtomicBool flags and guard checks) is conditionally compiled
only when the std feature is enabled, ensuring zero overhead for bare-metal targets.
Why is LockPolicy::free taking metadata?
The free method requires the Meta object returned by try_lock. For simple locks
(like Atomic), Meta is just (), making the release trivial. However, for more
complex locks (like ticket locks or OS futexes that need to track waiter queues), this
metadata is essential to correctly release the exact lock instance or wake the correct
threads.
Because the Meta is tied to the specific acquisition, it also prevents accidentally
releasing a lock you don’t own or releasing it multiple times with stale state. For
simple locks where Meta = (), calling free(&()) on an already free lock remains a
guaranteed no-op, which dramatically simplifies the implementation of composite locks
(like lock::Nested) and error-handling paths. If an abort occurs halfway through
acquiring a nested lock, the cleanup code can safely call free on all inner locks
that successfully returned their metadata.
Why lock::Nested?
Deadlocks often occur when multiple locks are acquired in inconsistent orders across
different threads. lock::Nested enforces a strict, deterministic acquisition order
(L1 then L2) and a reverse release order (L2 then L1). This provides a
compile-time building block for safe multi-resource locking.
Why AcquireError instead of just PoisonError?
Standard library locks return PoisonError<Guard>, which conflates poisoning
with other potential lock failures. Resync separates these concerns using
AcquireError and TryLockError. These enums distinguish between:
- Poisoned: A previous thread panicked, and the data might be inconsistent.
- Lock: A fatal, unrecoverable error occurred in the underlying
LockPolicy(e.g., OS resource exhaustion). - Retry: The
RetryPolicyaborted the acquisition loop (e.g., due to a timeout).
This granular error handling allows no_std environments and complex systems
to react appropriately to timeouts or hardware failures without relying on
panics.
Why Shield instead of a custom RwLock?
Writer starvation is a common problem in RwLocks. Instead of hardcoding a
“writer-preference” mode into the base Os or Atomic locks (which adds
overhead to readers who don’t need it), Resync provides Shield. It acts as a
transparent wrapper that intercepts try_lock and try_share, dynamically
blocking readers only when a writer is actively waiting. This keeps the base
locks fast and simple while providing a composable solution for fairness.
5. Limitations and Caveats
While Resync is highly flexible, it is important to understand its boundaries:
- No Async/Await Support: Resync is strictly designed for synchronous,
thread-based, or interrupt-level synchronization. It does not integrate
with Rust’s
WakerorContextAPIs. Using spin-locks inside an async executor will block the executor thread and starve other futures. - Fairness is Not Guaranteed: The default
lock::Atomicuses a simplecompare_exchange. Under extreme contention, this can lead to thread starvation (where one thread repeatedly wins the race). If strict fairness is required, you must implement a customLockPolicy(e.g., a ticket lock or MCS lock). - Nightly vs. Stable: On stable Rust, traits and default implementations
cannot be
const. If you requireconstinitialization of your locks in static variables, you must compile your crate with a nightly toolchain. Resync will automatically detect the nightly channel and enableconst_trait_implandconst_default. - Spin-Loop Starvation: If the thread holding the lock is preempted by
the OS while a waiting thread is executing a
retry::Busyloop, the waiting thread will burn CPU cycles until the OS reschedules the holder. Always preferretry::Yieldin user-space applications unless you are certain the critical section is shorter than a context switch. - Poisoning Requires
std: The lock poisoning mechanism relies onstd::thread::panicking()to detect unwinding. In#![no_std]environments, panics typically abort the process, making poisoning irrelevant. Thus, poisoning features are gated behind thestdfeature and add zero overhead to bare-metal targets.
6. Summary
Resync gives you the raw materials to build exactly the synchronization primitive you need, while enforcing safe, race-free API boundaries. By understanding the distinction between acquisition and waiting, you can optimize your concurrent code for any environment, from bare-metal microcontrollers to high-throughput user-space servers.