Skip to content

Shared Semaphore

SharedSemaphore

Rust Edition Layout Protocol Cross-Process

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 + ObservationRing and implements subetha_sidecar::AdaptiveInstance. Wrap in SidecarBox::new to register with the global sidecar; raw create() / open() return the unregistered type unchanged.

  • initial and max_permits set at create.

  • try_acquire is one CAS decrement; non-blocking.

  • release is one CAS increment; rolls back if the increment exceeds max_permits.

  • RAII Permit guard: drop releases automatically. Use mem::forget(permit) + manual release() 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.

OpSharedSemaphore (mmf)Arc<(Mutex<u32>, Condvar)>Relative
try_acquire (uncontended)2.02 ns16.84 ns8.32x faster
acquire + release (uncontended)16.07 ns46.51 ns2.89x faster
available()1.19 ns16.69 ns14x faster

Reading the trade-offs

  1. try_acquire 8.32x faster. One CAS decrement vs Mutex lock + check + decrement + unlock. The CAS dominates.
  2. acquire+release 2.89x faster. Two CAS ops vs full Mutex+Condvar dance.
  3. available 14x faster. One atomic load vs Mutex lock + read + unlock.
  4. 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::spawn inside b.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 + waiters provides 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 calling release(). Otherwise permits leak.

  • Releasing more than acquired. release rolls back if it exceeds max_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 vs Arc<(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.