Skip to main content

SyncRegion

Struct SyncRegion 

Source
pub struct SyncRegion<T> { /* private fields */ }
Available on crate feature std only.
Expand description

A thread-safe wrapper around Region<T> — the trusted concurrent baseline.

This is a coarse-grained std::sync::RwLock<Region<T>> with an ergonomic guard-based API: multiple readers (read) or one writer (write) at a time. It is the always-shippable concurrent answer: correct under any interleaving because every mutation serialises through the lock. Note: “correct under any interleaving” refers to safety/serialization — memory safety and API contract invariants — not to bounded writer latency. std::sync::RwLock does not guarantee portable fairness or starvation-freedom; on some platforms a new reader may be delayed behind an awaiting writer depending on OS scheduling. Finer-grained or lock-free alternatives are out of scope for this crate.

The wrapper stays #![forbid(unsafe_code)]: all interior mutability comes from std’s RwLock. Use read / write for multi-operation transactions (the borrows tie to the guard), or the one-shot convenience methods (insert, remove, …) which take &self and lock internally. reserve and capacity have no one-shot form — reach them through a held guard, e.g. sr.write().reserve(n) / sr.read().capacity().

§Poisoning policy

A panic while the write guard is held poisons the RwLockstd never poisons on a read-guard panic (e.g. a panicking T::clone inside get_cloned releases the read lock cleanly, with no poison). The container structure (Region/slotmap invariants) stays intact regardless of the panic — this crate guarantees no memory corruption. However, interior side effects are the responsibility of T: if T::clone modifies internal state (e.g. Cell, atomics, internal Mutex) before panicking, that modification persists. The RwLock itself does not poison on read-guard panic, so the SyncRegion remains usable.

Poison recovery guarantees container integrity only, not operation completion. The recovered Region has no memory corruption, but an interrupted operation may have left partial effects visible: a panicking T::Drop during clear() leaves the region partially cleared – container-valid and reusable, but the exact set of surviving values is an unspecified implementation detail of the underlying slotmap version’s unwind cleanup, not a stable contract (see Region::clear’s own documentation), and a panicked multi-op write() transaction leaves whatever partial effects it already applied. Callers whose T carries cross-value invariants, or whose multi-op transactions need all-or-nothing semantics, must implement their own signaling — this crate provides none beyond what’s documented here.

Poison is cleared on recovery. read and write (and therefore every one-shot convenience method, which locks internally) clear the lock’s poison flag immediately after recovering from it. This is a deliberate policy consistent with “poison recovery guarantees container integrity only” above: since this crate already always trusts the container after ANY recovery, permanently forcing every subsequent access down std::sync::RwLock’s slower poisoned-recovery path would cost real performance for no additional safety — the container was already proven sound on the FIRST recovery. One consequence: SyncRegion never exposes an is_poisoned() check, because poisoned state is never observable for longer than the single access that first recovers from it (except through Debug, which reports it without clearing). Applications that need a durable, observable “a writer panicked here” signal for their own cross-value invariants must implement it themselves (e.g. an AtomicBool alongside the SyncRegion) — this crate’s own poison flag is not a substitute, by design.

§Reentrancy

get_cloned runs T::clone, and clear runs each T::Drop, while the internal lock is held. If T’s Clone or Drop implementation re-enters the same SyncRegion (directly or transitively), the thread deadlocks or panics per std::sync::RwLock’s documented same-thread reacquisition behavior. Even non-reentrant but slow Clone/Drop delays every other user: clear holds the write lock across its entire linear sweep, while get_cloned holds the read lock across the clone (readers are unaffected by the latter, but writers block). Never call a one-shot convenience method (or a nested read/write) while the calling thread already holds a read/write guard from the same SyncRegion — the one-shots lock internally and the nested acquisition deadlocks (std’s RwLock is not reentrant; even read-after-read can block behind a queued writer, since the platform’s priority policy is unspecified).

§Contended reads

Under multi-threaded read contention, the one-shot convenience methods (get_cloned, contains, len, is_empty) anti-scale: each call pays a shared-cache-line lock acquisition that dominates the nanosecond-scale lookup. Historical measurement (harness: examples/contended_reads.rs, regime: 8 readers, one-shot vs. batched reads on a noisy dev host) showed a ~4× aggregate throughput loss at 8 readers and ~30× speedup from batching. A later, more rigorous gate (docs/perf/R828_STRUCTURAL_LEVERS_GATE.md §2) measured the same question with a different harness (r828_batch_guard_probe.rs) and found 9.15× — explicitly recorded as an open discrepancy, not silently reconciled. The numbers above are retained for historical context; do not treat them as a stable measurement of batching’s benefit without consulting the R828 gate’s analysis.

§Async runtimes

SyncRegion uses blocking std::sync::RwLock, which is not async-aware. In an async context (e.g. tokio), this has concrete hazards:

  • Holding a guard across .await blocks the executor worker for the entire await duration, not just the critical section. The worker cannot schedule other tasks while blocked.

  • One-shot methods (get_cloned, insert, remove, …) synchronously block the OS thread — they are not “async-safe just because they’re fast.” Contention can still stall an executor worker.

  • tokio::time::timeout does NOT cancel a blocking lock acquisition. The timeout fires after the operation completes (or never fires if the lock is held forever), but the blocking wait itself cannot be interrupted.

  • spawn_blocking does NOT make an already-started operation cancellation-safe. Once a blocking call is in flight on a worker thread, dropping the JoinHandle does not abort it.

For async-friendly ownership, use an async lock type such as tokio::sync::RwLock or async_rwlock instead of this wrapper. Guard-batching (see the contended reads section above) is valid only within a synchronous section — it does not make blocking safe in async code.

Implementations§

Source§

impl<T> SyncRegion<T>

Source

pub fn into_inner(self) -> Region<T>

Extracts the inner Region<T>, consuming this SyncRegion.

This is the inverse of From<Region<T>> for SyncRegion<T>. It provides a zero-cost conversion from concurrent back to single-threaded usage, preserving all handles (they remain valid in the extracted Region<T>).

If the RwLock is poisoned (due to a panic in a writer thread), this method recovers the Region anyway — the container structure is guaranteed intact, and T’s invariants are the caller’s responsibility (see the poisoning policy for full details).

Source§

impl<T> SyncRegion<T>

Source

pub fn new() -> Self

Creates an empty region that allocates nothing until first use.

§Panics

Delegates to Region::new — see its # Panics section for the exact conditions (process-wide region_id counter exhaustion).

Source

pub fn with_capacity(capacity: usize) -> Self

Creates an empty region with space pre-reserved for capacity entries.

§Panics

Delegates to Region::with_capacity — see its # Panics section for the exact conditions (capacity limit, allocation size overflow, and process-wide region_id counter exhaustion).

Source

pub fn read(&self) -> RwLockReadGuard<'_, Region<T>>

Locks for shared read, returning a guard that hands out &Region<T>.

Multiple readers may hold the guard concurrently. Recovers from poison and clears it (see the poisoning policy’s “Poison is cleared on recovery” note) so a single past writer panic does not permanently slow every future access. Returns std’s own guard type directly — a deliberate, stable API commitment; migrating the internal lock implementation in the future would be a breaking change.

Source

pub fn write(&self) -> RwLockWriteGuard<'_, Region<T>>

Locks for exclusive write, returning a guard that hands out &mut Region<T>.

Blocks all other readers and writers until dropped. Recovers from poison and clears it (see the poisoning policy’s “Poison is cleared on recovery” note) so a single past writer panic does not permanently slow every future access. Returns std’s own guard type directly — a deliberate, stable API commitment; migrating the internal lock implementation in the future would be a breaking change.

Source

pub fn insert(&self, value: T) -> Handle<T>

Inserts value, returning a fresh handle that resolves to it (I1).

One-shot convenience that locks for write internally. For a transaction that does several ops under one lock, use write instead.

§Panics

Panics if the backing slotmap is full (2^32 - 2 live entries).

Source

pub fn remove(&self, handle: Handle<T>) -> Option<T>

Removes and returns the value for handle, or None if stale/removed.

One-shot convenience that locks for write internally. The write guard is released before the removed value is dropped by the caller, so a reentrant Drop on the removed value is safe against the deadlock class described in the reentrancy section.

Source

pub fn contains(&self, handle: Handle<T>) -> bool

Whether handle currently resolves to a live value.

One-shot convenience that locks for read internally. Note that under concurrency a true result may be stale by the time the caller acts on it; acting on a stale handle can only ever produce None at the point of use, never resolve to a wrong live value within roughly 2^31 reuse cycles of that slot. Callers who need an atomic check-then-act use write; callers who only need an atomic check-then-read can use the cheaper read instead — either way, one held guard instead of two separate lock acquisitions.

Source

pub fn len(&self) -> usize

Number of live values (I4).

One-shot convenience that locks for read internally. Note that under concurrency the count is a momentary snapshot, not a stable property.

Source

pub fn is_empty(&self) -> bool

Whether the region holds no live values (I4).

One-shot convenience that locks for read internally. Note that under concurrency this is a momentary snapshot, not a stable property.

Source

pub fn clear(&self)

Removes every value, invalidating all outstanding handles.

One-shot convenience that locks for write internally. The partial-clear-under-panic contract is Region::clear’s — see there.

§Reentrancy hazard

If T::Drop attempts to acquire a read or write lock on the same SyncRegion, a deadlock will occur (the lock is already held for write). Prefer extracting values to a temporary container and dropping them after the lock is released.

Source

pub fn get_cloned(&self, handle: Handle<T>) -> Option<T>
where T: Clone,

Clones the value for handle out without leaving the caller holding a guard, or None if stale/removed. One-shot convenience that locks for read internally.

Prefer this over read when you only need a by-value copy and don’t want to hold the guard across other work. Note that the T::clone call itself runs under the read lock (this is unavoidable due to borrowing semantics), so for expensive-Clone payloads every call extends the lock hold by the full clone duration and delays any writer arriving during that window by up to that much (measured ~1.5–1.8 ms worst-case writer stall for a 4 MiB payload). For such payloads, store Arc<T> instead so the “clone” is a cheap refcount bump.

See the reentrancy section for the deadlock hazard when T::clone re-enters the same SyncRegion.

Trait Implementations§

Source§

impl<T> Debug for SyncRegion<T>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<T> Default for SyncRegion<T>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<T> From<Region<T>> for SyncRegion<T>

Source§

fn from(region: Region<T>) -> Self

Wraps an existing Region<T> in a SyncRegion for safe concurrent access.

This provides a zero-copy conversion path from single-threaded to concurrent usage without invalidating existing handles — all Handle<T> values from the original Region remain valid and resolve correctly in the wrapped SyncRegion.

Source§

impl<T> From<SyncRegion<T>> for Region<T>

Source§

fn from(sr: SyncRegion<T>) -> Self

Converts to this type from the input type.

Auto Trait Implementations§

§

impl<T> !Freeze for SyncRegion<T>

§

impl<T> RefUnwindSafe for SyncRegion<T>

§

impl<T> Send for SyncRegion<T>
where T: Send,

§

impl<T> Sync for SyncRegion<T>
where T: Send + Sync,

§

impl<T> Unpin for SyncRegion<T>
where T: Unpin,

§

impl<T> UnsafeUnpin for SyncRegion<T>

§

impl<T> UnwindSafe for SyncRegion<T>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.