Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku Kernel Semaphores: Counting, Timeouts, and Safe Shutdown

Use Haiku kernel semaphores for bounded synchronization with correct counts, timeout handling, waiter shutdown, and explicit lifetime coordination.

Haiku’s kernel semaphore API provides a count that threads can acquire and release. It is a useful primitive for producer-consumer coordination, bounded work slots, and waiting for a condition that another thread signals. It is not automatically a mutex, condition variable, event queue, or proof that a shared object is safe to access. The application must choose what a count represents and enforce a consistent protocol around it.

The lifecycle is explicit: create a semaphore with an initial count and name, acquire one or more units, release units when the application protocol permits, and delete the semaphore when no thread can still use its ID. Most concurrency bugs arise not from the syscall itself but from mismatched counts, lost shutdown signals, unhandled timeout results, or deleting the object while waiters still depend on it.

Model the count before writing code

Write down what one unit means. For a queue with a semaphore initialized to zero, one release might represent one queued item. For a fixed resource pool, the initial count might represent available slots. For a binary gate, the application may restrict itself to values zero and one, but the kernel API still exposes a count-based object. A counting semaphore does not track an owning thread the way a mutex does.

Before each release, establish that the event or resource represented by that unit really exists. If a worker consumes a wakeup but finds no corresponding item, either the producer and consumer protocol is incorrect or the semaphore is serving a different purpose than the code claims. Do not use a semaphore count as a substitute for inspecting protected queue state; synchronize that state separately.

Use names that identify purpose and ownership, not transient pointer values. Names help diagnostics but do not establish identity, permissions, or lifetime. Keep the sem_id in an object whose shutdown protocol is clear, and prevent stale IDs from being reused after deletion.

Check creation and every wait result

create_sem(count, name) returns a semaphore ID or an error. Validate the result before storing it. acquire_sem_etc() allows a caller to request a count and a timeout mode; a relative timeout uses B_RELATIVE_TIMEOUT. Handle success, timeout, deletion or other failures as distinct cases. A timeout means the caller did not obtain the requested count; it is not permission to proceed as though the resource or event exists.

sem_id workReady = create_sem(0, "worker queue ready");
if (workReady < B_OK)
    return workReady;

status_t status = acquire_sem_etc(workReady, 1,
    B_RELATIVE_TIMEOUT, 5LL * 1000000);
if (status == B_OK) {
    // Re-check the protected queue or condition before consuming work.
} else if (status == B_TIMED_OUT) {
    // The timeout elapsed; do not assume an item was acquired.
} else {
    // Handle shutdown or another failure according to the protocol.
}

The snippet uses a five-second relative timeout in microseconds. A production worker should also have a cancellation or shutdown path and should check the queue after waking, because a wakeup is a synchronization signal, not the work item itself. Include the appropriate kernel and status headers for the target Haiku SDK.

acquire_sem() waits without an explicit timeout. Use it only when an unbounded wait is truly intended and another path guarantees progress or shutdown. Otherwise use the timed form and define how the caller behaves after timeout. Do not spin by immediately retrying a timed wait in a tight loop; that turns a safety timeout into CPU consumption.

Pair release operations with real state transitions

release_sem() releases one unit. release_sem_etc() accepts a count and flags for additional behavior, but flags such as release-all alter the protocol and should be used only when the design explicitly needs them. Review the public header before using system-oriented flags; several semaphore flags are documented for system use only.

For a bounded queue, a robust ordering is to place the item into the queue under the queue’s lock and then signal the semaphore. The consumer acquires a unit and removes an item under the same state lock. If shutdown uses a special sentinel, represent that sentinel unambiguously and release enough waiters for the intended number of workers. Keep queue length and semaphore count aligned through every error path, including allocation failure and rejected work.

Never release merely because a thread is exiting if the release does not correspond to a defined event. Spurious count growth can make future acquisitions pass when no work or slot exists. If the implementation must wake all waiters during shutdown, set a separate stopped state first, then wake them in a documented way and have each waiter re-check that state after acquiring.

Timeouts and interruption are control flow

The API distinguishes relative and absolute timeout modes. A relative deadline is often appropriate for a bounded wait measured from the call. An absolute deadline can be useful when a larger operation has a fixed budget and repeated waits should not reset that budget. Convert units carefully and avoid overflowing bigtime_t when computing an absolute deadline.

Do not copy system-only interrupt flags into application code. If a wait returns an interruption or other error, follow the target API contract and decide whether to retry, cancel, or report failure. An unconditional retry can hide a shutdown signal. Log the status code and the operation being waited for, not just a generic “semaphore failed” message.

Synchronize shutdown and deletion

delete_sem() is a lifecycle operation, not a normal wakeup. Before deleting, stop producers, prevent new waiters, publish a stopping state, and ensure every thread that can call acquire or release has either exited or will no longer use the ID. If a worker is blocked indefinitely, design a deliberate wakeup path and join it before deletion. The owner object should remain alive until those joins finish.

Guard against double deletion and stale IDs by centralizing ownership. A small wrapper can store the ID, make destruction idempotent at the application layer, and expose an explicit close operation that reports status. Do not pass raw IDs to arbitrary components without a lifetime contract. If the semaphore owner changes through set_sem_owner(), define which team is responsible for cleanup and access.

Choose a semaphore only when its semantics fit

Use a lock when the core requirement is exclusive ownership of shared state. Use a semaphore when counting available resources or blocking until a countable event is appropriate. A semaphore does not automatically protect a multi-field data structure, guarantee a particular fairness policy, or make UI calls safe from a worker thread. Combine it with a lock or message passing when the invariant requires one.

For callback-driven media or device code, keep wakeups bounded and make teardown observable. A callback that blocks indefinitely can stall an entire processing chain. Prefer a nonblocking handoff or a bounded queue when the callback’s timing budget is strict, and test under both overload and shutdown conditions.

Failure-oriented verification

Test a zero-count wait, successful acquire, multiple-unit acquire, timeout, release during wait, shutdown during wait, deletion after all users exit, producer failure before release, queue insertion failure, and repeated shutdown. Verify that no worker consumes nonexistent work, no count leaks after cancelled tasks, and no thread accesses a deleted semaphore ID. Run stress tests with several producers and consumers and assert the queue’s own invariants independently of semaphore operations.

For diagnostics, record the semaphore purpose, requested count, timeout mode, status, and shutdown state. Avoid treating a name as a unique ID. Make deadlock reports include thread ownership and wait location where possible so a blocked worker can be distinguished from an empty queue or a deliberately closed service.

Acceptance criteria

Accept a semaphore design when each count has one documented meaning, acquire and release correspond to real state changes, timeout and error paths are explicit, shared state has its own protection, and shutdown joins users before deletion. Verify the protocol under overload, interruption, and repeated teardown.

The Haiku semaphore API is a low-level synchronization primitive. Correctness still depends on the application’s count invariant, lock ordering, cancellation model, and resource lifetime.

Related:

Sources:

Comments