Haiku BLocker and BAutolock: Recursive Locks, Timeouts, and Lifetime
Use Haiku BLocker and BAutolock with correct recursion, timeout, and object-lifetime rules to prevent deadlocks in multithreaded applications.
BLocker is Haiku’s Support Kit mutex-like primitive for protecting shared state across threads. It supports recursive acquisition by the same thread, can wait with a timeout, and offers BAutolock for scope-based release. Those conveniences do not remove the need for a clear ownership protocol. Every successful acquisition must be balanced, objects must outlive all waiters, and a recursive lock does not prevent deadlocks involving another thread or another lock.
Use BLocker to protect a small invariant, not to make an entire object “thread-safe” by implication. Document which fields it protects, which methods require the lock, and whether a caller may enter a method while already holding it. That contract matters more than the internal implementation choice.
Understand recursion and pair each acquisition
The same thread can acquire a BLocker recursively. Each successful Lock() increases the held recursion count; each Unlock() releases one level. This allows a locked method to call another method that uses the same locker, but it can also conceal call graphs that are difficult to reason about.
class Counter {
public:
void Add(int32 amount)
{
BAutolock guard(fLock);
if (!guard.IsLocked())
return;
fValue += amount;
UpdateMaximum(fValue);
}
private:
void UpdateMaximum(int32 value)
{
BAutolock guard(fLock);
if (!guard.IsLocked())
return;
if (value > fMaximum)
fMaximum = value;
}
BLocker fLock{"counter-state"};
int32 fValue = 0;
int32 fMaximum = 0;
};
Because BLocker is recursive, UpdateMaximum() can lock the same object again on the calling thread. The helper remains safe when called independently, but the recursion should be intentional and documented. Do not depend on recursion to compensate for unclear lock ownership across a large call graph. A refactor that replaces BLocker with a nonrecursive primitive could otherwise expose a latent self-deadlock.
BAutolock’s constructor attempts to acquire the BLocker, IsLocked() reports whether it succeeded, and its destructor releases the lock if held. Check IsLocked() before touching protected state. Do not call Unlock() on a BAutolock object that failed acquisition and assume it will acquire automatically; use its Lock() method only when the control flow deliberately needs a later attempt.
Make timeouts part of the failure path
LockWithTimeout() takes a relative timeout in microseconds and returns a status_t. A timeout is not the same type as the boolean result from Lock(). Check for B_OK before accessing protected data, and treat timeout as a normal operational outcome when the caller has a bounded latency requirement.
status_t UpdateCache(bigtime_t timeout)
{
status_t status = fLock.LockWithTimeout(timeout);
if (status != B_OK)
return status;
RebuildSmallCache();
fLock.Unlock();
return B_OK;
}
In production code, ensure every path after LockWithTimeout() succeeds reaches exactly one Unlock(). An early return added later can leak the lock and block other threads indefinitely. Prefer a scope guard for multi-exit code; if using BAutolock, note that its public constructors use ordinary Lock() and do not accept a timeout. Do not pass a millisecond count where the API expects microseconds. A timeout of 250000 represents a quarter of a second.
A timeout does not fix a deadlock. It only lets one caller stop waiting after a bounded interval. If the same lock consistently times out, record the owner and call path, reduce the critical section, or correct the lock order. Do not retry in a tight loop; that converts a deadlock or contention problem into CPU use and log noise.
Establish lock order and keep critical sections bounded
Two locks can still deadlock even when each one is recursive. Suppose thread A holds lock X and waits for Y, while thread B holds Y and waits for X. Neither thread is recursively acquiring its own lock, so recursion provides no escape. Define a global order for nested locks and acquire them consistently. Where possible, copy the small shared state needed under one lock, release it, and perform work afterward.
Do not hold a BLocker while waiting for disk I/O, network replies, a user callback, another looper, or a condition that may require another thread to acquire the same lock. A lock should protect an invariant for a short, predictable interval. Logging can also block; capture minimal state while locked and format or emit diagnostics after releasing the lock.
IsLocked() answers whether the calling thread holds the lock, not whether some thread in the process holds it. LockingThread(), CountLocks(), and CountLockRequests() can help diagnose ownership and contention, but snapshots can become stale immediately. Treat them as diagnostics, not synchronization predicates. Never write “if not locked, then lock” using IsLocked() as a race-free acquisition protocol; call Lock() or LockWithTimeout() and check its result.
Use a name in the BLocker constructor. Haiku’s debugging tools can use that identity to make deadlocks and lock waits easier to interpret. Keep names within the API’s documented length limit and distinguish instances when a class contains several different locks.
Respect destruction and waiter lifetime
The BLocker object must stay alive while any thread can call it or is waiting on it. Destroying a locker with pending Lock() requests cancels those requests, and Lock() reports failure; that is not a normal object shutdown strategy. First stop new work, signal worker threads, join them, and only then destroy the state and lock they might access.
A common race is to protect an object’s fields with an internal BLocker while a worker still has a raw pointer to that object. The lock cannot protect the object’s lifetime before the worker acquires it. Use a separate ownership protocol, reference counting, message-based serialization, or another mechanism that proves the object remains alive for the whole call.
Do not expose a BLocker pointer without explaining who owns it and how long it remains valid. A caller that stores a pointer past owner shutdown can use freed memory before it ever reaches Lock(). Likewise, a method that returns a reference to protected data after unlocking has returned an unprotected alias. Copy data while locked or use an explicit lifetime-safe object.
Choose BLocker versus a looper’s lock deliberately
BLooper and BWindow have their own locking and message-dispatch model. BAutolock can wrap either a BLocker or a BLooper, but the locks protect different contracts. A looper lock serializes access to handlers and window state with that looper’s event processing. A BLocker protects application data according to the application’s design. Substituting one for the other can introduce lock ordering bugs or bypass required UI-thread sequencing.
Never call a synchronous operation on a looper while holding a BLocker if that looper may need the same locker to complete the call. Conversely, do not enter arbitrary external callbacks while holding a window lock. Write down ordering between the app’s data lock and any BLooper lock, then keep every code path consistent with that order.
If work belongs on a looper, post a message containing a copy of the required data instead of holding a BLocker while directly mutating a view from a worker. Message passing often provides a simpler serialization boundary than protecting UI state with another mutex.
Use acceptance checks that expose races
Stress the protected invariant with multiple worker threads and a high iteration count. Add a test path that intentionally holds the lock long enough for LockWithTimeout() to time out, and assert that the caller does not touch protected state after failure. Test nested methods that recursively acquire the same lock, then test those helpers independently.
Exercise shutdown while workers are active: stop accepting work, wake blocked threads, join them, and verify no thread touches the locker after destruction. Add diagnostics for timeout counts and lock hold duration, but avoid high-volume logging inside the critical section. Review every return statement after successful acquisition and ensure it releases once.
BLocker supplies mutual exclusion and same-thread recursion, not object lifetime, fairness, transaction semantics, or freedom from lock-order deadlock. Pair acquisitions carefully, use BAutolock for straightforward scopes, handle timeouts as real errors, and keep the protected invariant explicit. Those practices make synchronization predictable as the application grows.
Related:
- Haiku Teams, Threads, Ports, and the Kernel Object Model
- Haiku BMessenger Delivery: Target Identity, Replies, Timeouts, and Shutdown
Sources: