runtime: Add Go-style spinner accounting to the scheduler
Track how many threads per threadpool are "spinning" (busy-polling the
scheduler queues after failing to pop work), following the protocol of
the Go runtime:
* at most half of a pool may spin at once; a thread denied a spinner
slot parks immediately instead of polling, so an idle pool converges
to a bounded set of searchers instead of a herd hammering the queues
of whichever thread is producing work
* jl_wakeup_threadpool does not wake anyone while a spinner exists in
the pool; the spinner will find the work. A spinner leaving to park
decrements the count before its sleeping-store + fence + queue
recheck, pairing with the enqueuer store + fence + count load, so the
skip cannot lose a wakeup
* the last spinner to exit with work wakes a successor, so a burst of
enqueues absorbed by one spinner still fans out
On a 64-core machine this removes the idle-polling herd entirely, with
the existing Partr scheduler unchanged: 100k-task fan-out at -t64
improves 9.3x (882 -> 95 ms), fine-grained parallel-for 7.5x, token
ring at -t128 30x (1206 -> 40 ms), channel ping-pong at -t64 4.4x.
The cost is slower ramp-up from a fully parked pool (serial wake
propagation), visible as a small regression on coarse imbalanced
workloads at high thread counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>