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

Haiku pthread Cancellation: Deferred Requests, Cleanup, and Condition Waits

Trace Haiku pthread cancellation through pending flags, condition-wait mutex reacquisition, LIFO cleanup, and explicitly audited cancellation points.

pthread_cancel() is a request to terminate a particular thread, not an immediate safe-stop primitive. Correct code must account for when that request can be acted upon, which invariants may be temporarily broken, and how locks or other resources are restored during termination. Haiku’s implementation is useful to study because its pthread source exposes the cancellation flag, kernel interruption path, deferred test, condition-wait wrapper, and cleanup stack separately.

POSIX distinguishes cancellation state from cancellation type. State determines whether cancellation is enabled; type determines whether an enabled request is deferred until a cancellation point or acted on asynchronously. The Open Group describes deferred cancellation as the normal model and warns that asynchronous cancellation can interrupt code that is not async-cancel-safe. Haiku initializes a new pthread with cancellation enabled but deferred. Portable code should preserve that default unless it has a narrowly justified and audited reason to do otherwise.

Follow the request through Haiku’s implementation

At the reviewed Haiku commit, pthread_cancel() atomically sets a canceled flag. If the target had cancellation enabled, Haiku calls _kern_cancel_thread() with a callback. That callback exits the thread only when asynchronous cancellation is enabled; for a deferred target, the kernel-side interruption is instead what lets an interruptible operation return to the library’s cancellation check. If cancellation was disabled when requested, the request remains pending. With deferred type it is acted upon when the thread later reaches a cancellation point or calls pthread_testcancel() after re-enabling cancellation.

pthread_testcancel() checks the pending and enabled bits and calls pthread_exit(PTHREAD_CANCELED) when both are set. This makes cancellation cooperative at explicit points: a CPU-bound loop that never reaches a cancellation point or calls pthread_testcancel() should not be expected to stop promptly under deferred cancellation. Adding a test point is a design decision because it defines where the program can safely abandon its current work.

The source also shows pthread_cond_wait() wrapping its internal wait with RETURN_AND_TEST_CANCEL. The internal condition-wait routine checks mutex ownership, registers the waiter, releases the caller’s mutex while blocking, then reacquires it before returning. The cancellation test occurs after that helper returns. This ordering matters: cleanup code entered by cancellation can rely on the condition wait’s documented mutex reacquisition, provided the caller registered a cleanup handler that restores the surrounding invariant.

Use cleanup handlers to make cancellation a state transition

The portable pattern is to establish cleanup before reaching a cancellation point that may block while a resource is held. The following is an illustrative pattern; it omits queue initialization, error handling, and application-specific state:

static void unlock_mutex(void *arg)
{
    pthread_mutex_unlock((pthread_mutex_t *)arg);
}

void *worker(void *arg)
{
    pthread_mutex_lock(&queue_lock);
    pthread_cleanup_push(unlock_mutex, &queue_lock);

    while (!stopping && queue_is_empty())
        pthread_cond_wait(&queue_changed, &queue_lock);

    pthread_cleanup_pop(1);
    return NULL;
}

On the ordinary path, pthread_cleanup_pop(1) executes the unlock handler. If cancellation is acted upon inside the condition wait, POSIX requires the mutex to be reacquired before cancellation cleanup handlers are called; the handler can then release it. The cleanup stack is LIFO, so nested resources should be registered in the reverse order needed for teardown. The code must still handle ordinary errors and avoid returning from inside the lexical scope created by the cleanup macros.

Haiku’s cleanup implementation links each handler to a per-thread stack. Its thread-destruction routine repeatedly pops and invokes handlers, then calls thread-specific-data destructors. This is an implementation detail that aligns with the portable LIFO model, but it does not remove the application’s responsibility to register the correct cleanup before a cancellation point. A thread that owns a resource but has not installed a suitable handler can still leave shared state inconsistent.

Avoid treating asynchronous cancellation as a shortcut

Asynchronous cancellation can arrive between two operations that together maintain an invariant: after a lock is acquired but before its cleanup is registered, during allocator bookkeeping, or while a data structure is half-updated. POSIX requires async-cancel safety only for a small subset of interfaces and says that cancellation during a non-async-cancel-safe function has undefined behavior. A callback that merely calls pthread_exit() is not evidence that arbitrary application code is safe to terminate at an arbitrary instruction.

For a long-running worker, prefer a shutdown flag protected by the same synchronization scheme as its work queue, signal the condition variable, and let the thread leave through its ordinary control flow. Use pthread_cancel() only when the target’s cancellation points and cleanup behavior are understood. If code temporarily changes cancellation state or type, save the old value and restore it on every ordinary exit path. Do not switch to asynchronous mode to make a blocked operation appear responsive without checking every resource boundary in that interval.

Validate the exact Haiku release

An implementation source review cannot prove all runtime behavior. On a disposable Haiku VM, test a deferred request against (1) a thread blocked in pthread_cond_wait(), (2) a CPU loop with and without pthread_testcancel(), (3) a request made while cancellation is disabled and then re-enabled, and (4) nested cleanup handlers. Assert that the condition mutex is owned when its cleanup handler runs, that handlers execute in reverse push order, and that the joined result is PTHREAD_CANCELED when cancellation is acted upon. Also test target exit racing with pthread_cancel() and record the returned error.

The source review here is pinned to one Haiku commit and checks only selected functions. It does not establish that every POSIX cancellation point has a Haiku wrapper, that every release has identical internals, or that a native runtime test passed. Audit the actual system headers and implementation for the release you deploy, and use the POSIX specification for portability requirements rather than extrapolating from one source file.

Related:

Sources:

Comments