Module sync/semaphore

sync/semaphore
Stability: stable — `new`, `acquire`, `try_acquire`, `release`, `with_permit` and `available_permits` follow Java's and `tokio`'s counting-semaphore surface. The two properties most likely to be mistaken for provisional are deliberate and documented above: acquisition is NOT fair, and the permit count is not capped, which is what makes this usable as a signalling device rather than only as a lock. `available_permits` is a snapshot. — stable modules only change additively; this one may still change.

Counting semaphore — Dijkstra's P/V, the same model Java's java.util.concurrent.Semaphore and Rust's tokio::sync::Semaphore carry.

A semaphore holds a non-negative-by-convention count of permits. acquire() takes one permit, blocking while none is available; release() returns one. The count is not capped: release() may be called more times than acquire(), which simply raises the number of permits in flight (this is what makes a semaphore usable as a signalling device and not only as a lock). It is likewise not bound to a thread: the releaser need not be the acquirer, which is precisely what distinguishes it from Mutex.

Acquisition is not fair. A thread that calls acquire() while a permit happens to be available takes it on the spot, even if other threads have been blocked in acquire() for longer — the same barging behaviour as Java's default (non-fair) constructor. There is no FIFO guarantee, and a waiter can in principle be starved under sustained contention. Build fairness on top if you need it.

Typical use is bounding concurrency — at most K threads inside a region:

{ Semaphore } :: import "std/sync/semaphore";
{ Thread } :: import "std/thread";

sem := Semaphore.new(i32(2)); // at most 2 workers in the region
t := Thread(unit).spawn((io) => {
  sem.acquire();
  // ... at most 2 threads are ever here ...
  sem.release();
});
t.join();

Stability

stable — new, acquire, try_acquire, release, with_permit and available_permits follow Java's and tokio's counting-semaphore surface. The two properties most likely to be mistaken for provisional are deliberate and documented above: acquisition is NOT fair, and the permit count is not capped, which is what makes this usable as a signalling device rather than only as a lock. available_permits is a snapshot.

Types

Semaphore atomic object
Semaphore

Counting semaphore built on Mutex and Cond. Uses atomic reference counting for safe cross-thread sharing.

The permit count lives under _mutex rather than in an atomic: every mutation has to be serialized with the _permits <= 0 observation made by a waiter in acquire, otherwise a permit released between a waiter's test and its wait() would be lost.

Fields

NameTypeDescription
_permitsi32
_mutexMutex(bool)
_cvCond
impl(Semaphore, ...)
new : (Semaphore) fn(permits : i32) -> Semaphore

Create a semaphore holding permits permits.

A negative initial count is legal and means "the first -permits + 1 releases unblock nothing", the classic way to make N threads wait for a batch of work.

Parameters

NameTypeNotes
permitsi32

Returns: Semaphore

acquire : (Semaphore) fn(self : Semaphore) -> unit

Take one permit, blocking until one is available.

Parameters

NameTypeNotes
selfSemaphore

Returns: unit

try_acquire : (Semaphore) fn(self : Semaphore) -> bool

Take one permit if one is available right now. Never blocks. Returns true when a permit was taken.

Parameters

NameTypeNotes
selfSemaphore

Returns: bool

release : (Semaphore) fn(self : Semaphore) -> unit

Return one permit, waking one waiter if any is blocked in acquire.

signal() and not broadcast(): one permit can satisfy exactly one waiter, since acquire never takes more than one. A wakeup that loses the race to a thread that never blocked is harmless — the loser re-tests _permits in its while loop and parks again, and the permit it lost was consumed by the winner, so no release goes unmatched.

Parameters

NameTypeNotes
selfSemaphore

Returns: unit

with_permit : (Semaphore) fn(generic(R) self : Semaphore, body : Impl : (Fn() -> R)) -> R

Hold one permit for the duration of body, releasing it on the way out.

The manual acquire() / release() pair is the one that leaks a permit the first time an early exit is added to the block between them, and a leaked permit is a deadlock the next time the pool fills. This is the same guard shape Mutex.with_lock uses, for the same reason.

The release rides a Dispose guard, so it happens on any scope exit the language can produce here. It does NOT claim unwind safety: body is a closure, and a closure cannot capture a control-bound value, so body cannot raise an effect that unwinds past this call at all. Taking the effect as a parameter is what would make such a claim testable — the same point plans/STD_API_STABILIZATION.md §4 makes about async/mutex.with_lock. See issues/with-lock-and-with-permit-cannot-see-an-unwind.md.

pool.with_permit(() => fetch(url));

Parameters

NameTypeNotes
selfSemaphore
bodyImpl : (Fn() -> R)

Returns: R

available_permits : (Semaphore) fn(self : Semaphore) -> i32

The permits available at the instant of the call. Informational only — by the time the caller looks at it another thread may have taken one.

Parameters

NameTypeNotes
selfSemaphore

Returns: i32