pub struct SyncRegion<T> { /* private fields */ }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 RwLock — std 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
.awaitblocks 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::timeoutdoes 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_blockingdoes NOT make an already-started operation cancellation-safe. Once a blocking call is in flight on a worker thread, dropping theJoinHandledoes 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>
impl<T> SyncRegion<T>
Sourcepub fn into_inner(self) -> Region<T>
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>
impl<T> SyncRegion<T>
Sourcepub fn new() -> Self
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).
Sourcepub fn with_capacity(capacity: usize) -> Self
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).
Sourcepub fn read(&self) -> RwLockReadGuard<'_, Region<T>>
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.
Sourcepub fn write(&self) -> RwLockWriteGuard<'_, Region<T>>
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.
Sourcepub fn remove(&self, handle: Handle<T>) -> Option<T>
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.
Sourcepub fn contains(&self, handle: Handle<T>) -> bool
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.
Sourcepub fn len(&self) -> usize
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.
Sourcepub fn is_empty(&self) -> bool
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.
Sourcepub fn clear(&self)
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.
Sourcepub fn get_cloned(&self, handle: Handle<T>) -> Option<T>where
T: Clone,
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>
impl<T> Debug for SyncRegion<T>
Source§impl<T> Default for SyncRegion<T>
impl<T> Default for SyncRegion<T>
Source§impl<T> From<Region<T>> for SyncRegion<T>
impl<T> From<Region<T>> for SyncRegion<T>
Source§fn from(region: Region<T>) -> Self
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.