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

Haiku Thread-Local Storage: Keys, Initialization, and Cleanup

Use Haiku TLS keys as process-wide slots for per-thread state, with checked allocation, explicit initialization, pointer ownership, and thread cleanup.

Haiku’s Support Kit thread-local storage API associates a value with the current thread under an integer key. It is useful when a library needs function-local access to per-thread context without threading an explicit argument through every call. A key is allocated once and reused by every participating thread; each thread then sets and reads its own value at that index.

The public API is small: tls_allocate() creates a key, tls_get() reads the current thread’s value, tls_set() writes it, and tls_address() returns a pointer to the current thread’s slot. The header limits the process to TLS_MAX_KEYS, currently 64. That limit is shared across the process, including libraries, so a TLS key is not a cheap per-task or per-request allocation.

Allocate keys once, not per thread

Allocate a key during controlled component initialization and retain the returned index. Do not call tls_allocate() every time a worker starts. The API has no matching public free operation, and its documentation says allocation should normally happen once for a global index that can be reused by every thread. A library should allocate only the keys it needs, check for B_NO_MEMORY, and fail initialization cleanly if the finite process-wide pool is exhausted.

Because keys are ordinary integers, protect their lifecycle at the component level. Do not expose an uninitialized global index to callers before allocation succeeds. If initialization can be called concurrently, use a one-time initialization mechanism. A default numeric value such as zero is not a safe “unallocated” sentinel unless the API explicitly guarantees it cannot be a valid key.

static int32 sRequestContextKey = -1;

status_t InitializeRequestContextTLS()
{
    if (sRequestContextKey >= 0)
        return B_OK;

    int32 key = tls_allocate();
    if (key < 0)
        return B_NO_MEMORY;

    sRequestContextKey = key;
    return B_OK;
}

void SetCurrentRequestContext(void* context)
{
    if (sRequestContextKey >= 0)
        tls_set(sRequestContextKey, context);
}

This simple guard assumes initialization is serialized. If several threads may call it simultaneously, protect allocation with std::call_once or the project’s equivalent and publish the key only after success. Do not assume that tls_set() reports errors; it returns void, and the API documentation warns that an invalid index can lead to unpredictable results.

Initialize every thread that uses the key

TLS values are per-thread. Setting a value in one worker does not set it for another worker, and a new thread starts without your application-specific context unless you install one. The support documentation’s example allocates keys in a manager, then explicitly initializes each thread in its entry path.

Make thread initialization part of the worker wrapper rather than relying on every call site to remember it. This is especially important for thread pools: workers are reused for many requests, so state from one task can leak into the next unless it is reset or replaced at every task boundary. Use a scoped guard that saves, sets, and restores the prior value if nested calls are allowed.

tls_get() returns the value associated with the current thread, or null for no value or an invalid key. Null is therefore ambiguous if null is a legitimate stored value. If the distinction matters, store a wrapper object or a separate initialization flag. Do not treat tls_get() returning null as proof that key allocation itself failed.

Treat TLS pointers as borrowed current-thread state

The tls_address() API returns a pointer to the current thread’s slot, so direct writes through it affect that thread’s value. Do not retain the returned pointer and use it from another thread. It does not point to a process-global cell. Obtain it on the thread that will use it and keep it only as long as the key and thread-local slot remain valid.

The value is a void*; TLS does not own the pointed-to object, call a destructor, or synchronize that object’s members. Decide who allocates and frees each context. If the value points to a heap object, arrange cleanup in the thread’s exit path or when a pooled worker discards its context. Avoid storing pointers to stack variables that outlive the stack frame.

Library TLS is particularly vulnerable to stale pointers when threads are created by an external runtime. If your component does not control the thread entry function, expose explicit AttachCurrentThread() and DetachCurrentThread() operations and require the host to call them. Do not assume Haiku TLS automatically runs C++ thread-local destructors for values you place there; it is a raw pointer slot API.

Avoid confusing per-thread context with synchronization

TLS removes contention over the slot value because each thread has its own slot, but it does not make shared objects referenced by that pointer thread-safe. Two TLS slots can point to the same global object. If they do, that object’s own synchronization rules still apply.

TLS is also not a replacement for passing explicit context where that produces clearer APIs. A hidden “current request” variable can make nested calls, asynchronous callbacks, and tests depend on ambient state. Prefer an explicit parameter when the context is part of the function’s semantics. Use TLS for cross-cutting context that must be available through a deep call stack and whose thread boundary is well defined.

Do not use TLS to communicate from one thread to another. A worker’s value is not a message queue, and setting a key in one thread does not publish a new value to another. Send a message, use a port, or protect an explicit shared structure instead. Similarly, do not use a TLS integer key as an authorization or identity proof; it only selects a slot in the current process.

Handle thread reuse and shutdown

For a long-lived worker pool, define whether context persists for the worker lifetime or is reset for every job. If it persists, remove request-specific fields before accepting another request. If it is job-scoped, install it immediately before dispatch and clear or restore it in a finally-style cleanup path even when work fails or throws.

For a dedicated thread, free the context object in the thread’s exit path before the thread terminates. If thread termination can be initiated externally, use a cleanup callback or owner-managed join protocol so the object is not leaked. Do not free a context from the main thread while a worker can still read its slot.

Since TLS keys have process-wide scarcity and no public free call, design key allocation as a library resource budget. Consolidate related fields into one context object instead of allocating a separate TLS key for every small value. Document key owners and count them in tests that load multiple libraries; one component’s allocation pressure affects every other component in the same process.

Verify the API with failure-oriented tests

Test key-allocation failure, missing initialization, per-thread isolation, nested context restoration, pooled-worker reuse, null values, thread exit, thread restart, and shutdown while a callback still uses the context. Confirm that a thread cannot accidentally inherit another worker’s request data. Check that all context-owned allocations are freed exactly once.

Run tests with several concurrent threads and include a deliberately invalid key only in a subprocess or dedicated failure harness, because the public documentation warns that invalid indices can have unpredictable results. Measure the remaining shared-object synchronization separately; TLS itself does not solve races in the data it references.

Haiku TLS is a process-wide finite key table for per-thread pointer values. Allocate keys once, initialize each participating thread explicitly, preserve ownership of pointed-to data, and clear or free context during the correct thread lifecycle. When the context can be passed explicitly, prefer the visible parameter; when TLS is justified, keep it small, documented, and budgeted.

Related:

Sources:

Comments