Module sync/semaphore
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
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
| Name | Type | Description |
|---|---|---|
_permits | i32 | |
_mutex | Mutex(bool) | |
_cv | Cond |
impl(Semaphore, ...)
new : (Semaphore) fn(permits : i32) -> SemaphoreCreate 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
| Name | Type | Notes |
|---|---|---|
permits | i32 |
Returns: Semaphore
acquire : (Semaphore) fn(self : Semaphore) -> unitTake one permit, blocking until one is available.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Semaphore |
Returns: unit
try_acquire : (Semaphore) fn(self : Semaphore) -> boolTake one permit if one is available right now. Never blocks.
Returns true when a permit was taken.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Semaphore |
Returns: bool
release : (Semaphore) fn(self : Semaphore) -> unitReturn 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
| Name | Type | Notes |
|---|---|---|
self | Semaphore |
Returns: unit
with_permit : (Semaphore) fn(generic(R) self : Semaphore, body : Impl : (Fn() -> R)) -> RHold 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
| Name | Type | Notes |
|---|---|---|
self | Semaphore | |
body | Impl : (Fn() -> R) |
Returns: R
available_permits : (Semaphore) fn(self : Semaphore) -> i32The 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
| Name | Type | Notes |
|---|---|---|
self | Semaphore |
Returns: i32