2026-07-14
Regular futexes are policy-free: the kernel puts waiters to sleep and wakes them, but doesn't care who holds the lock. That's a problem for real-time code. If a low-priority thread holds a mutex and a high-priority thread blocks on it, a medium-priority thread can preempt the low-priority holder indefinitely — the high-priority thread waits on a lock that will never be released. This is unbounded priority inversion, and it's what famously stalled the Mars Pathfinder rover in 1997.
FUTEX_LOCK_PI fixes this by making the kernel aware of ownership. The user-space contract changes: the futex word stores the TID of the owner in its lower 30 bits, with bit 31 (FUTEX_WAITERS) set when others are blocked and bit 30 (FUTEX_OWNER_DIED) set if the holder crashed. Fast path in userspace is still a single CAS from 0 to your TID. Only on contention do you syscall into FUTEX_LOCK_PI.
Inside the kernel, the waiter is enqueued on a PI-aware rt_mutex, and the scheduler boosts the owner's effective priority to match the highest-priority waiter. The boost propagates transitively: if the owner is itself blocked on another PI-futex, that chain's owner gets boosted too. When the owner calls FUTEX_UNLOCK_PI, its priority is restored and the highest-priority waiter is handed ownership atomically (the kernel writes the new TID into the futex word for you — you don't get to race).
Concrete example: A financial trading system runs an audit thread at nice -20 (SCHED_FIFO priority 80), a market-data thread at priority 40, and a background compression thread at priority 10. Compression grabs a stats mutex, then the audit thread blocks on it. Without PI, the market-data thread — at priority 40 — preempts compression forever, and the audit thread misses its deadline. With PTHREAD_PRIO_INHERIT set on the mutex, glibc uses FUTEX_LOCK_PI: compression is boosted to priority 80, finishes its critical section, drops back to 10, and audit proceeds.
Rule of thumb: PI-futex uncontended fast path is the same ~20 ns CAS as a normal futex. Contended path costs roughly 2–3x a normal FUTEX_WAIT (extra rt_mutex bookkeeping and priority boost propagation). Use PTHREAD_PRIO_INHERIT only on mutexes shared across priority bands — for locks between threads at the same priority, it's pure overhead.
Pair PI-futexes with the robust futex list (a prior lesson) and the kernel will also set FUTEX_OWNER_DIED if the holder is killed mid-critical-section, letting the next waiter recover instead of deadlocking forever.
