threadpool
nimony/lib/std/threadpool.nim
const StripeSize: int64Tasks per stripe; must be a power of 2.
const BulkSize: int64Max tasks drained per bulk dequeue.
const StageSize: int64Tasks held in a worker's private staging ring; must be a power of 2.
type Task = object con: Continuation
proc toTask(c: Continuation): Taskproc poolPollIo(timeoutMs: int64): boolPolls one I/O backend once; returns true if it delivered anything.
std/ioringregisters one of these.type WorkerMetrics = object enqueueMiss: uint64 dequeueMiss: uint64 tasksSubmited: uint64 tasksHandled: uint64 tasksStealed: uint64
var workerMetrics: seqvar workerCount: int64var gReactor: proc (timeoutMs: int64): bool {.nimcall.}var gReactorState: int640 until
gReactor/gReactorWaits— and everything they reach — are fully set up. It has to be PUBLISHED rather than merely assigned:initPoolstarts the workers, and only then doesioring.setupRingallocate the per-lane arenas the reactor indexes and finally pointgReactorat itself. Nothing orders those plain stores against each other, so on a weakly ordered machine a worker could see the new reactor and an arena seq that is still empty.reactorReadypairs an acquire withsetupRing's release.var gReactorWaits: boolDoes
gReactor(ms)actually sleep for up toms? Set from the I/O backend (seeBackendRelays.waits). While it is false an idle worker has to do the sleeping itself, because a reactor that only peeks would otherwise leave the loop spinning on a core.var threadIdx: int64proc submit(t: Task; h: int64)Submit a task to the pool. Non-lossy with "caller-runs" backpressure: try the hinted stripe, then the others (absorbing bursts); if every stripe is full, run the continuation inline — trampolining it on the calling thread and handing the remainder back to the pool the moment a slot frees — rather than dropping it or blocking.
Why caller-runs and not a blocking wait: workers re-submit continuations from inside the trampoline (see
workerLoop). A blockingsubmitcould park every worker on a full queue with none left to drain it -> deadlock. Caller-runs instead guarantees forward progress (a producer that outruns the pool simply does the work), so the queue stays bounded atStripeCount*StripeSizeand no submitter ever stalls.proc submit(c: Continuation; hint: int64)Convenience: submit a bare continuation as a task. On a worker with the default hint, the task is first staged on the private lock-free ring and handed to the shared stripes in bulk on the next worker cycle (see
flushStaged); everything else enqueues immediately.proc poolHelp(): boolproc reactorReady(): boolHas the I/O ring published its reactor? See
gReactorState.proc reactorPoll(timeoutMs: int64): boolDrive the I/O reactor if there is one, and the do-nothing default until there is. Every caller of
gReactorgoes through this.proc initPool()proc isPoolWorker(): boolTrue on a thread created by
initPool.threadIdxis only meaningful as an identity on those: it is a threadvar defaulting to 0, so every foreign thread (the main thread included) reports 0 — the same value worker 0 uses. Anything that indexes per-thread state bythreadIdxmust ask this first.proc stopped(): boolproc shutdownPool()