Commit f6dfd35
Add cede() for yielding the current virtual thread (#481)
Adds `cede()` to `ox.control`, next to `checkInterrupt()`:
```scala
inline def cede(): Unit =
checkInterrupt()
Thread.`yield`()
```
Virtual threads are not preempted, so a CPU-bound loop can starve other
virtual threads (in an experiment with 2x-cores busy-looping VTs, half
of them received *zero* CPU time). `cede()` gives compute-intensive code
a way to cooperate with the scheduler — and, since it also checks the
interrupt flag, with the cancellation protocol: a busy loop calling
`cede()` becomes both fair and cancellable.
## Why `checkInterrupt + Thread.yield`
Based on experiments comparing yielding techniques on virtual threads
(JDK 21 & 26, Linux, 8 cores), following up on [this
gist](https://gist.github.com/lbialy/9f3732ce9901ba76ee2da9dd86f7df33):
* `Thread.yield` genuinely unmounts and resubmits the continuation to
the back of the scheduler's queue (external submit when the carrier's
local queue is empty) — it reliably prevents starvation (89–93%
liveness-probe capture vs 2% baseline), at ~1µs per call.
* `LockSupport.parkNanos(1)` gives near-perfect round-robin fairness,
but costs ~40–46µs per call (timer round-trip), stalls the caller for
that time, and silently degrades to a no-op when the interrupt flag is
set — i.e. exactly when the enclosing scope is being cancelled. Since
`Thread.yield` is ~50x cheaper, calling it more frequently (every ~1ms
of computation) matches `parkNanos(1)`'s liveness at a quarter of the
overhead. The trade-off is mentioned in the scaladoc.
* `LockSupport.parkNanos(0)` is a true no-op.
* `Thread.sleep(0)` behaves identically to `checkInterrupt +
Thread.yield` (that's literally its JDK implementation for virtual
threads), but only as an `@implNote` — the explicit version doesn't rely
on unspecified behaviour.
The interrupt check makes `cede()` a cancellation boundary, consistent
with e.g. cats-effect's `IO.cede`.
Includes tests and a docs entry in `utils/control-flow.md`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent fd71b52 commit f6dfd35
3 files changed
Lines changed: 43 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
71 | 92 | | |
72 | 93 | | |
73 | 94 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
11 | 15 | | |
12 | 16 | | |
0 commit comments