scheduler: Add a work-stealing scheduler backend
Add Base.Scheduler.Workstealing, a work-stealing task scheduler
following the design shared by the Go runtime and Tokio:
* a bounded FIFO ring of task references per worker thread with a
packed (steal, real) head, so batch steals are visible to other
thieves and index wraparound cannot be confused with an empty queue;
the owner pushes with no atomic RMW and pops with a single CAS (no
seq-cst fence, unlike a Chase-Lev deque)
* a per-thread LIFO slot holding the most recently enqueued task for
latency (capped at 3 consecutive polls to avoid starving the ring,
and stealable so a busy owner cannot strand it)
* thieves steal half of a victim ring in one claim, sweeping victims
from their own successor so concurrent thieves fan out, copying raw
slots into their own buffer with a single tail publish
* a per-threadpool injection queue for cross-pool/foreign enqueues and
ring overflow, striped over independently locked segments so an
off-pool producer and the pool workers do not serialize on one lock;
consumers refill in batches proportional to queue length
* a fairness tick serving the injection queue every 61 polls so tasks
respawning each other cannot starve injected work
* consumed slots and links are always cleared so queues never pin dead
tasks
The scheduler is selected via Base.Scheduler.ChosenScheduler; Partr
remains available as the alternative backend. Idle-thief throttling is
provided by the runtime spinner accounting added separately; on top of
it, the queues further improve recursive fork-join (task-per-node
fib 2x at -t8, 3.2x at -t128 oversubscribed) and channel ping-pong
(1.9x at -t64) over Partr on a 64-core machine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Amended: the claim protocol must wake a pinned task's thread directly.
A task blocked in fetch/take! hosts its thread's sleep logic, keeping its
tid pinned while the thread parks; when a dequeuer's claim fails it
re-parks the task in the injection queue, and the pool wake it issued was
gated by its own spinner slot — the task then cycles through the injection
queue indefinitely while its only eligible thread sleeps (observed as a
livelocked spinner at ~10M iterations/s with the rest of the pool parked).
tryclaim now delivers the task to the pinned thread's sticky workqueue
and wakes that thread directly (the mailbox pattern, as enq_work does for
sticky tasks), keeping it out of the shared queues entirely,
mirroring cd463b86b9 (#62371); a regression test drives the hosted-consumer
pattern under a watchdog.