ReviewOS

home-lang/home

fix(threading): replace spin-only primitives with futex-backed Mutex/CondVar/RwLock/Semaphore

#802
Closed chris opened this 8 days ago ·
8 days ago

Problem

Every primitive in packages/threading busy-waits on spinLoopHint, and CondVar is not a condition variable: signaled is a plain bool with no atomics, broadcast wakes at most one waiter, a signal with no waiter is remembered, and the timeout is a guessed spin count. Recursive mutexes are silently ignored. Only one test uses more than one thread. packages/memory's ThreadSafe allocator is built on this spin mutex. The runtime already has real Futex.zig/Mutex.zig/Condition.zig under packages/runtime/src/threading/.

Evidence (origin/main @ d4b219507)

Scope

  • Build all primitives on a shared futex layer (reuse the runtime's, or std.Io primitives).
  • Implement recursive mutexes or reject the flag.
  • Real timeouts based on the monotonic clock.

Acceptance criteria

  • Stress tests with ≥8 threads for lock, condvar wait/signal/broadcast, rwlock and semaphore pass under ThreadSanitizer.
  • Blocked waiters consume no CPU (measured).

Part of #814.

8 days ago

Semaphore blocking slice landed on origin/main in 23b1fcf (fix(threading): block semaphore waiters on futexes).\n\nImplemented:\n- packages/threading now consumes Home's existing cross-platform runtime futex through an explicit named build module.\n- The shared futex no longer imports the runtime root merely for assertions, so it is independently reusable.\n- Semaphore.wait blocks in the OS while the permit count is zero rather than spinning.\n- Each successful post wakes at most one waiter after publishing the permit.\n- Added an eight-thread zero-permit contention regression that verifies every posted permit wakes exactly one worker and the final count returns to zero.\n- Updated the Linux futex backend from stale linux.E.init calls to Zig 0.17's linux.errno API, exposed by cross-target compilation.\n\nVerification:\n- Native threading: 27/29 passed, only Linux/Windows affinity runtime checks skipped; guarded peak 192 MB on the final run.\n- Shared futex native unit tests: 3/3 passed, 170 MB peak.\n- Complete threading module graph cross-compiled for x86_64-linux and x86_64-windows.\n- Scoped Pickier and diff checks passed.\n- Exactly 4 files changed.\n\n#802 stays open. This slice does not claim ThreadSanitizer coverage or measured blocked-waiter CPU utilization, and mutex/condition-variable/rwlock/barrier blocking work remains.

8 days ago

Mutex blocking slice landed on origin/main in 0770fb7 (fix(threading): block contended mutexes on futexes).\n\nImplemented:\n- Replaced the standalone atomic spin loop with the shared futex-backed 0/1/contended state machine.\n- Uncontended tryLock uses a strong CAS; contended lock calls block in the OS and unlock wakes one waiter.\n- Unlocking an already-unlocked mutex now reports MutexUnlockFailed.\n- Recursive MutexAttr is explicitly rejected with OperationNotSupported instead of being silently ignored.\n- Added an eight-thread protected-counter contention regression plus public-facade recursive-mode coverage.\n\nVerification:\n- Native threading: 30/32 passed, only Linux/Windows affinity runtime checks skipped; guarded peak 194 MB.\n- Complete threading graph cross-compiled for x86_64-linux and x86_64-windows.\n- Downstream memory package: 18/18 passed, guarded peak 174 MB.\n- Scoped Pickier and diff checks passed; exactly 3 files changed.\n\n#802 remains open for condition-variable, rwlock, and barrier blocking, plus TSan and measured blocked-CPU acceptance gates.

8 days ago

Condition-variable blocking slice landed on origin/main in 9f4d72a (fix(threading): block condition waits on futexes).\n\nImplemented:\n- Replaced the non-atomic signaled boolean and spin loop with a monotonic futex epoch.\n- wait observes the epoch, unlocks atomically with respect to the value check, blocks in the OS, and reacquires the mutex before returning.\n- waitTimeout uses the futex backend's relative OS timeout and always reacquires the mutex before returning false.\n- signal increments the epoch and wakes one waiter; broadcast wakes all current waiters.\n- Signals with no waiter are not remembered by future waits.\n- Added regressions for no-waiter signal + timeout/reacquisition, one-waiter signal, eight-waiter broadcast, and the public facade.\n\nVerification:\n- Native threading: 34/36 passed, only Linux/Windows affinity runtime checks skipped; guarded peak 184 MB.\n- Complete threading graph cross-compiled for x86_64-linux and x86_64-windows.\n- Scoped Pickier and diff checks passed; exactly 3 files changed.\n\n#802 remains open for rwlock and barrier blocking plus TSan and measured blocked-CPU acceptance gates.

8 days ago

Read/write-lock blocking slice landed on origin/main in 5bcbbe0 (fix(threading): block read-write locks on futexes).\n\nImplemented:\n- Replaced spin-only reader/writer coordination with a futex-backed state word.\n- Multiple readers share the low-bit count; one exclusive writer owns the high bit.\n- A separate waiting-writer count closes the reader gate once a writer queues, preventing an endless reader stream from starving writers.\n- Zero-state transitions wake all because futex wake-one cannot choose a writer and could deadlock by waking a gated reader.\n- Invalid/unbalanced unlocks now return RwLockUnlockFailed.\n- Added eight-concurrent-reader coverage, mixed four-reader/four-writer protected-state stress, and public facade shared-reader coverage.\n\nVerification:\n- Native threading: 37/39 passed, only Linux/Windows affinity runtime checks skipped; guarded peak 161 MB.\n- Complete threading graph cross-compiled for x86_64-linux and x86_64-windows.\n- Scoped Pickier and diff checks passed; exactly 3 files changed.\n- The guarded native run waited for the existing machine lock, so no heavy processes overlapped.\n\n#802 remains open for barrier blocking plus TSan and measured blocked-CPU acceptance gates.

8 days ago

Barrier blocking slice landed on origin/main in 861653684 (fix(threading): block barrier waiters on futexes).\n\nImplemented:\n- Replaced generation spin-waiting with futex blocking.\n- The last arrival resets the count, advances the generation, and wakes the full cohort.\n- Waiters loop on the generation so spurious wakes cannot release a barrier early.\n- Zero-party barriers now return InvalidArgument instead of deadlocking forever.\n- Added an eight-thread, 100-generation regression (800 synchronized waits) plus single-party public facade coverage.\n\nVerification:\n- Native threading: 40/42 passed, only Linux/Windows affinity runtime checks skipped; guarded peak 167 MB.\n- Complete threading graph cross-compiled for x86_64-linux and x86_64-windows.\n- Scoped Pickier and diff checks passed; exactly 3 files changed.\n- Cross-target work respected the shared machine lock and did not overlap the other guarded workload.\n\nSemaphore, mutex, condition variable, rwlock, and barrier waits are now all OS-blocking. #802 remains open because its acceptance criteria still require ThreadSanitizer repetition and measured blocked-waiter CPU utilization; neither is claimed here.

8 days ago

ThreadSanitizer acceptance progress landed on origin/main in 7b987ac (test(threading): repeat contention suite under TSan).\n\nChanges:\n- Strengthened condition-variable signal coverage from one waiter to eight waiters, released one at a time with a protected ticket predicate.\n- Added executable scripts/threading-tsan.sh, defaulting to 100 complete native TSan iterations and printing full compiler/test output only on failure.\n- Documented the exact guarded reproduction command.\n\nMeasured result on this host:\n- 100/100 complete native threading runs passed under Zig -fsanitize-thread.\n- Each run covered the ≥8-thread semaphore, mutex, condvar signal/broadcast, rwlock, and reusable barrier stress cases.\n- Zero ThreadSanitizer race reports and zero test failures.\n- Guarded peak process-tree footprint: 203 MB; host low-water: 33%.\n- Strengthened normal native suite: 40/42 passed with only Linux/Windows affinity runtime skips, 165 MB peak.\n- x86_64-linux and x86_64-windows graphs cross-compiled after the test change.\n- Shell syntax, Zig formatting for the changed Zig file, scoped Pickier, and diff checks passed. shellcheck is not installed, so that gate is not claimed.\n- Exactly 3 files changed.\n\nThe repeated TSan acceptance item is now satisfied. #802 remains open only for an explicit measured blocked-waiter CPU-utilization result.

8 days ago

Completed on origin/main by commit 5f1238d (test(threading): measure blocked waiter CPU use).

Both acceptance criteria are now satisfied:

  • ThreadSanitizer: commit 7b987ac completed 100/100 full native threading runs with zero race reports and zero failures. Every run exercised at least eight threads across mutex, condition-variable signal/broadcast, read/write lock, semaphore, and barrier contention.
  • Blocked waiter CPU: commit 5f1238d adds a repeatable per-thread CLOCK_THREAD_CPUTIME_ID measurement. Across five runs with eight semaphore waiters blocked for 250 ms, the worst waiter consumed 32 us of CPU. The failure threshold is 12,500 us, or 5% of the blocked interval.
  • The five-run measurement peaked at 82 MB under a 512 MB guard.
  • The final native threading build/test passed all 3 steps with a 123 MB guarded peak.
  • The measurement graph also cross-compiled for x86_64-linux.
  • Bash syntax, Zig formatting, scoped Pickier, and staged diff checks passed.
  • Exactly 3 files changed.

Closing #802 as complete. Remaining threading platform work is tracked separately in #803.

Sign in to comment on this issue.