You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
4. **Double-check**: This is critical to avoid the race where a reason was added just before `waiting_on_address` was set. If `wait_reasons` is now non-zero, clear `waiting_on_address` and go to step 1.
7. **Post-check**: Check `wait_reasons`. If non-zero, handle the reasons.
73
-
8. **Return**: Return the result of the wait to the caller.
74
-
- If `ret == ATOMICS_WAIT_OK`, return `0`.
75
-
- If `ret == ATOMICS_WAIT_TIMED_OUT`, return `-ETIMEDOUT`.
76
-
- If `ret == ATOMICS_WAIT_NOT_EQUAL`, return `-EWOULDBLOCK`.
77
-
78
-
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 that might not have changed the memory value.
60
+
The waiter will follow this logic:
61
+
62
+
1. **Notification Loop**:
63
+
```c
64
+
uintptr_t expected_null = 0;
65
+
while (!atomic_compare_exchange_strong(&self->wait_addr, &expected_null, (uintptr_t)addr)) {
66
+
// If the CAS failed, it means NOTIFY_BIT was set by another thread.
67
+
assert(expected_null == NOTIFY_BIT);
68
+
// Let the notifier know that we received the wakeup notification by
69
+
// resetting wait_addr.
70
+
self->wait_addr = 0;
71
+
handle_wakeup(); // Process mailbox or handle cancellation
72
+
// Reset expected_null because CAS updates it to the observed value on failure.
// Either the thread wasn't waiting (it will see NOTIFY_BIT later),
99
+
// or someone else is already in the process of notifying it.
100
+
return;
101
+
}
102
+
// We set the bit and are responsible for waking the target.
103
+
// The target is currently waiting on `addr`.
104
+
while (target->wait_addr == (addr | NOTIFY_BIT)) {
105
+
emscripten_futex_wake((void*)addr, INT_MAX);
106
+
sched_yield();
107
+
}
108
+
```
91
109
92
110
### 4. Handling the Race Condition
93
-
The "Lost Wakeup" race is handled by the combination of:
94
-
- The waiter double-checking `wait_reasons` after publishing its `waiting_on_address`.
95
-
- The waker looping `atomic.wake` until the waiter increments its `wait_counter`.
96
-
97
-
Even if the waker's first `atomic.wake` occurs after the waiter's double-check
98
-
but *before* the waiter actually enters the `atomic.wait` instruction, the waker
99
-
will continue to loop and call `atomic.wake` again. The subsequent call(s) will
100
-
successfully wake the waiter once it is actually sleeping.
101
-
102
-
Multiple wakers can safely call this logic simultaneously; they will all exit
103
-
the loop as soon as the waiter acknowledges the wake by incrementing the
104
-
counter.
105
-
106
-
### 5. Overlapping and Spurious Wakeups
107
-
The design must handle cases where "real" wakeups (triggered by the application) and "side-channel" wakeups (cancellation/mailbox) occur simultaneously.
108
-
109
-
1. **Spurious Wakeups for Other Threads**: If multiple threads are waiting on the same address (e.g., a shared mutex), a side-channel `atomic_wake(addr, 1)` targeted at Thread A might be delivered by the kernel to Thread B.
110
-
- **Thread B's response**: It will wake up, increment its `wait_counter`, see that its `wait_reasons` are empty, and return `0` to its caller.
111
-
- **Thread C (the waker)**: It will see that Thread A's `wait_counter` has *not* changed and `waiting_on_address` is still `addr`. It will therefore continue its loop and call `atomic_wake` again until Thread A is finally woken.
112
-
- **Result**: Thread B experiences a "spurious" wakeup. This is acceptable and expected behavior for futex-based synchronization.
113
-
2. **Handling Side-Channel Success**: If Thread A is woken by the side-channel, it handles the event and returns `0`. The user's code will typically see that its own synchronization condition is not yet met and immediately call `emscripten_futex_wait` again. This effectively "resumes" the wait from the user's perspective while having allowed the side-channel event to be processed.
114
-
3. **No Lost "Real" Wakeups**: By returning to the caller whenever `atomic.wait` returns `OK`, we ensure that we never miss or swallow a real application-level `atomic.wake`.
115
-
116
-
### 6. Counter Wrap-around
117
-
The `wait_counter` is a `uint32_t` and will wrap around to zero after $2^{32}$ wakeups. This is safe because:
118
-
1. **Impossibility of Racing**: For the waker to "miss" a wake-up due to wrap-around, the waiter would have to wake up and re-enter a sleep state exactly $2^{32}$ times in the very brief window between the waker's `atomic_wake` and its subsequent check of `wait_counter`. Even at extreme wakeup frequencies (e.g., 1 million per second), this would take over an hour.
119
-
2. **Address Change Check**: The waker loop also checks `target->waiting_on_address != target_addr`. If the waiter wakes up and either stops waiting or starts waiting on a *different* address, the waker will exit the loop regardless of the counter value.
120
-
121
-
### 6. Benefits
122
-
- **Lower Power Consumption**: Threads can sleep indefinitely (or for the full duration of a user-requested timeout) without periodic wakeups.
123
-
- **Lower Latency**: Mailbox events and cancellation requests are processed immediately rather than waiting for the next 1ms or 100ms tick.
124
-
- **Simpler Loop**: The complex logic for calculating remaining timeout slices in `emscripten_futex_wait` is removed.
111
+
The protocol handles the "Lost Wakeup" race by having the waker loop until the
112
+
waiter clears its `wait_addr`. If the waker sets the `NOTIFY_BIT` just before
113
+
the waiter enters `atomic.wait`, the `atomic_wake` will be delivered once the
114
+
waiter is asleep. If the waiter wakes up for any reason (timeout, real wake, or
115
+
side-channel wake), its `atomic_exchange` will satisfy the waker's loop
116
+
condition.
117
+
118
+
## Benefits
119
+
120
+
- **Lower Power Consumption**: Threads can sleep indefinitely (or for the full duration of a user-requested timeout) without periodic wakeups.
121
+
- **Lower Latency**: Mailbox events and cancellation requests are processed immediately rather than waiting for the next 1ms or 100ms tick.
122
+
- **Simpler Loop**: The complex logic for calculating remaining timeout slices in `emscripten_futex_wait` is removed.
125
123
126
124
## Alternatives Considered
127
-
- **Signal-based wakeups**: Not currently feasible in Wasm as signals are not
128
-
implemented in a way that can interrupt `atomic.wait`.
129
-
- **A single global "wake-up" address per thread**: This would require the
130
-
waiter to wait on *two* addresses simultaneously (the user's futex and its
131
-
own wakeup address), which `atomic.wait` does not support. The proposed
132
-
design works around this by having the waker use the *user's* futex address.
125
+
- **Signal-based wakeups**: Not currently feasible in Wasm as signals are not
126
+
implemented in a way that can interrupt `atomic.wait`.
127
+
- **A single global "wake-up" address per thread**: This would require the
128
+
waiter to wait on *two* addresses simultaneously (the user's futex and its
129
+
own wakeup address), which `atomic.wait` does not support. The proposed
130
+
design works around this by having the waker use the *user's* futex address.
133
131
134
132
## Security/Safety Considerations
135
-
- The `waiting_on_address` must be managed carefully to ensure wakers don't
136
-
call `atomic.wake` on stale addresses. The `wait_counter` and clearing the
137
-
address upon wake mitigate this.
138
-
- The waker loop should have a reasonable fallback (like a yield) to prevent a
139
-
busy-wait deadlock if the waiter is somehow prevented from waking up (though
140
-
`atomic.wait` is generally guaranteed to wake if `atomic.wake` is called).
133
+
- **The `wait_addr` must be managed carefully** to ensure wakers don't
134
+
call `atomic.wake` on stale addresses. Clearing the address upon wake
135
+
mitigates this.
136
+
- **The waker loop should have a reasonable fallback** (like a yield) to prevent a
137
+
busy-wait deadlock if the waiter is somehow prevented from waking up (though
138
+
`atomic.wait` is generally guaranteed to wake if `atomic.wake` is called).
0 commit comments