Shared Semaphore
SharedSemaphore
Cross-process counting semaphore. Permits stored in
AtomicU32; try_acquire is one CAS decrement; release is
one CAS increment with overflow rollback. RAII Permit guard
auto-releases on drop. Optional acquire blocks via a
spin/yield loop on the wakeup (a SharedAtomicU64) and
waiters (a SharedAtomicU32).
The “cross-process semaphore at lock-free cost” primitive. try_acquire at 2.02 ns vs
Arc<(Mutex<u32>, Condvar)>16.84 ns (8.32x faster). acquire+release cycle at 16.07 ns vs 46.51 ns (2.89x faster). available() at 1.19 ns vs 16.69 ns (14x faster). Architectural lever: cross-process permit counting at lock-free CAS speed.
Constraints (read first):
Native sidecar integration: the struct carries a
HandshakeHeader+ObservationRingand implementssubetha_sidecar::AdaptiveInstance. Wrap inSidecarBox::newto register with the global sidecar; rawcreate()/open()return the unregistered type unchanged.initialandmax_permitsset at create.try_acquireis one CAS decrement; non-blocking.releaseis one CAS increment; rolls back if the increment exceedsmax_permits.RAII
Permitguard: drop releases automatically. Usemem::forget(permit)+ manualrelease()to transfer ownership across an API boundary.3 MMF files: count + wakeup + waiters.
Cross-process backed by MMF.
Bench evidence
Bench harness: crates/subetha-cxc/benches/shared_semaphore.rs.
Captured 2026-06-02 on Windows 11 / Zen+ R7 2700, Criterion with
--sample-size=15 --warm-up-time=1 --measurement-time=2.
| Op | SharedSemaphore (mmf) | Arc<(Mutex<u32>, Condvar)> | Relative |
|---|---|---|---|
| try_acquire (uncontended) | 2.02 ns | 16.84 ns | 8.32x faster |
| acquire + release (uncontended) | 16.07 ns | 46.51 ns | 2.89x faster |
| available() | 1.19 ns | 16.69 ns | 14x faster |
Reading the trade-offs
- try_acquire 8.32x faster. One CAS decrement vs Mutex lock + check + decrement + unlock. The CAS dominates.
- acquire+release 2.89x faster. Two CAS ops vs full Mutex+Condvar dance.
- available 14x faster. One atomic load vs Mutex lock + read + unlock.
- Cross-process visibility is unique to the mmf primitive.
Rule 3b bench audit
- Fair contender:
Arc<(Mutex<u32>, Condvar)>is the textbook in-process semaphore. - No
thread::spawninsideb.iter: single-threaded; multi-thread contended acquire correctness in source tests. - MMF lifecycle managed: create + ops + drop + cleanup of 3 files.
What the numbers do NOT show
- Cross-process permit counting: any process can acquire / release; the mutex baseline cannot.
- Blocking acquire under contention: the spin/yield loop
on
wakeup+waitersprovides bounded-latency wakeups for cross-process waiters.
Worked examples
Bounded concurrency
use subetha_cxc::SharedSemaphore;
let sem = SharedSemaphore::create("/tmp/sem", 4, 4).unwrap(); // 4 permits
let permit = sem.acquire(); // RAII permit
do_bounded_work();
// permit dropped here -> release
Cross-process concurrency limit
// Each worker process (max_permits must match the creator's):
let sem = SharedSemaphore::open("/tmp/cluster-sem", 4).unwrap();
let _permit = sem.acquire(); // at most 4 across all processes
serve_request();Use case patterns
Pattern: cross-process concurrency cap
Limit total concurrent work across all participating processes to N permits.
Pattern: bounded-parallelism worker pool
Spawn workers up to N permits; each worker holds a permit while processing.
Pattern: barrier-like coordination
Acquire-only-when-N-released for batch fan-out / fan-in.
Known limitations
- Spin/yield wait: no kernel parking. High-contention workloads burn some CPU.
- u32 max permits: 4 billion cap (unreachable in practice).
- Cross-process backed by MMF.
Common pitfalls
Forgetting to release after
mem::forget(permit). The guard owns the release; manually forgetting it requires manually callingrelease(). Otherwise permits leak.Releasing more than acquired.
releaserolls back if it exceedsmax_permits; the permit count stays bounded but the caller’s logic is broken.Wrapping in a Mutex. Pointless; the CAS protocol IS the synchronization.
References
- Source:
crates/subetha-cxc/src/shared_semaphore.rs(625 lines, unit tests covering try/acquire/release cycle, RAII drop semantics, max_permits overflow rejection, cross-handle visibility, and acquire-blocks-until-release). - Bench:
crates/subetha-cxc/benches/shared_semaphore.rs(try_acquire, acquire+release, available vsArc<(Mutex<u32>, Condvar)>). - Sibling primitive: SHARED_RW_LOCK.md - reader-writer specialization (1-writer or N-readers).
- Sibling primitive: SHARED_RATE_LIMITER.md - token-bucket variant with refill.