Path: blob/main/docs/design/01-precise-futex-wakeups.md
14365 views
Design Doc: Precise Futex Wakeups
Status: Completed
Bug: https://github.com/emscripten-core/emscripten/issues/26633
Context
Historically, emscripten_futex_wait (in system/lib/pthread/emscripten_futex_wait.c) relied on a periodic wakeup loop for pthreads and the main runtime thread. This was done for two primary reasons:
Thread Cancellation: To check if the calling thread had been cancelled while it was blocked.
Main Runtime Thread Events: To allow the main runtime thread (even when not the main browser thread) to process its mailbox/event queue.
The old implementation used a 1ms wakeup interval for the main runtime thread and a 100ms interval for cancellable pthreads. This led to unnecessary CPU wakeups and increased latency for events.
Goals
Remove the periodic wakeup loop from
emscripten_futex_wait.Implement precise, event-driven wakeups for cancellation and mailbox events.
Maintain the existing
emscripten_futex_waitAPI signature.Focus implementation on threads that support
atomic.wait(pthreads and workers).
Non-Goals
Main Browser Thread: Changes to the busy-wait loop in
futex_wait_main_browser_threadare out of scope.Direct Atomics Usage: Threads that call
atomic.waitdirectly (bypassingemscripten_futex_wait) remain un-interruptible.Wasm Workers: Wasm Workers do not have a
pthreadstructure, so they are not covered by this design.
Design
The core idea is to allow "side-channel" wakeups (cancellation, mailbox events) to interrupt the atomic.wait call by having the waker call atomic.wake on the same address the waiter is currently blocked on.
As part of this design, emscripten_futex_wait now explicitly supports spurious wakeups. i.e. it may return 0 (success) even if the underlying futex was not explicitly woken by the application.
1. struct pthread Extensions
A single atomic wait_addr field was added to struct pthread (in system/lib/libc/musl/src/internal/pthread_impl.h).
2. Waiter Logic (emscripten_futex_wait)
The waiter follows this logic:
Publish Wait Address:
Wait: Call
ret = __builtin_wasm_memory_atomic_wait32(addr, val, timeout).Unpublish:
Handle side effects: If the wake was due to cancellation or mailbox events, these are handled after
emscripten_futex_waitreturns (or internally viapthread_testcancelif cancellable).
Note: We do not loop internally if ret == ATOMICS_WAIT_OK. Even if we suspect the wake was caused by a side-channel event, we must return to the user to avoid "swallowing" a simultaneous real application wake.
3. Waker Logic (_emscripten_thread_notify)
When a thread needs to wake another thread for a side-channel event (e.g. enqueuing work or cancellation), it calls _emscripten_thread_notify:
4. Handling the Race Condition
The protocol handles the "Lost Wakeup" race by having the waker loop until the waiter clears its wait_addr. If the waker sets the NOTIFY_BIT just before the waiter enters atomic.wait, the atomic_wake will be delivered once the waiter is asleep. If the waiter wakes up for any reason (timeout, real wake, or side-channel wake), its reset of wait_addr to 0 will satisfy the waker's loop condition.
Benefits
Lower Power Consumption: Threads can sleep indefinitely (or for the full duration of a user-requested timeout) without periodic wakeups.
Lower Latency: Mailbox events and cancellation requests are processed immediately rather than waiting for the next tick.
Simpler Loop: The complex logic for calculating remaining timeout slices in
emscripten_futex_waitwas removed.
Alternatives Considered
Signal-based wakeups: Not currently feasible in Wasm as signals are not implemented in a way that can interrupt
atomic.wait.A single global "wake-up" address per thread: This would require the waiter to wait on two addresses simultaneously (the user's futex and its own wakeup address), which
atomic.waitdoes not support. The implemented design works around this by having the waker use the user's futex address.
Security/Safety Considerations
The
wait_addris managed carefully to ensure wakers don't callatomic.wakeon stale addresses. Clearing the address upon wake mitigates this.The waker loop has a yield to prevent a busy-wait deadlock if the waiter is somehow prevented from waking up (though
atomic.waitis generally guaranteed to wake ifatomic.wakeis called).