Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions lib/std/private/threadpool_impl.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#

## Virtual-thread worker pool.
##
## Included from `std/typedthreads`.
##
## Workers never exit. createThread pops one off the idle stack (or
## pthread_create / CreateThread a new one when the stack is empty), hands
## it a task via a per-worker BinSem, and the worker runs it, pushes itself
## back onto the idle stack, and posts the requesting virtual thread's
## `done` flag. joinThread waits on that flag — `pthread_join` is not used.

{.push stackTrace: off.}

const
PoolWorkerStackMask = 1024 * 256 * sizeof(int) - 1
PoolWorkerStackSize* = PoolWorkerStackMask + 1 - 4096
## Matches typedthreads' default ThreadStackSize for non-embedded targets.

type
TaskEntry* = proc (closure: pointer) {.nimcall, gcsafe, raises: [].}
## Worker-side entry: static function pointer plus opaque closure.
## typedthreads' adapter builds the closure from `TArg`.

Worker* = object
osThread: SysThread
wake: BinSem
# Per-worker TLS-emulation storage. With virtual threads on this worker
# all `var x {.threadvar.}` resolve to fields in `gcThread.tls` — so the
# storage persists across V's on the same worker. This is what makes
# `var allocator {.threadvar.}: MemRegion` keep its chunks across
# createThread/joinThread cycles.
gcThread: GcThread
# Task slot. Written by `poolDispatch` while the worker is parked on
# `wake`; read by `workerMain` once it wakes.
entry: TaskEntry
closure: pointer
base: ptr ThreadBase
next: ptr Worker

# ---------------- idle stack ----------------

var
poolLock: SysLock
idleTop: ptr Worker

initSysLock(poolLock)

proc pushIdle(w: ptr Worker) =
acquireSys(poolLock)
w.next = idleTop
idleTop = w
releaseSys(poolLock)

proc popIdle(): ptr Worker =
acquireSys(poolLock)
result = idleTop
if result != nil:
idleTop = result.next
result.next = nil
releaseSys(poolLock)

# ---------------- worker main ----------------

proc workerMain(arg: pointer) {.nimcall, gcsafe, raises: [].} =
let w = cast[ptr Worker](arg)

# One-shot per-worker setup. The stack-bottom mark is a frame in
# workerMain — every task call frame sits at a higher (deeper) address,
# so the bottom we register here remains valid for the worker's whole
# process-lifetime. Emulated-TLS bootstrap (`globalsSlot`) is done
# earlier, inside the C-callable thunk, since any Nim-convention proc
# call may touch threadvars in its prologue.
when not defined(boehmgc) and not defined(gogc) and not defined(gcRegions) and
not defined(gcDestructors) and not defined(gcHooks):
# Mirrors `usesDestructors` in system.nim (not exported, so duplicated).
var stackMark {.volatile.}: pointer
nimGC_setStackBottom(addr(stackMark))
when declared(initGC):
initGC()
when declared(threadType):
threadType = ThreadType.NimThread

while true:
waitBinSem(w.wake)
# Snapshot the task slot locally before pushing back to idle, since the
# slot can be overwritten as soon as another caller pops this worker.
let entry = w.entry
let closure = w.closure
let base = w.base
try:
entry(closure)
except CatchableError:
discard
# Per-V destruction handlers fire here and the seq is cleared so V+1
# starts with a fresh handler list (decision 1).
when declared(nimThreadDestructionHandlers):
for i in countdown(nimThreadDestructionHandlers.len-1, 0):
try: nimThreadDestructionHandlers[i]()
except CatchableError: discard
nimThreadDestructionHandlers.setLen 0
# `pushIdle` BEFORE `postBinSem(done)` so a join-then-redispatch can land
# on the same worker. Reversing order is correctness-preserving but
# silently degrades reuse.
pushIdle(w)
postBinSem(base.done)

# ---------------- platform-specific spawn ----------------

when defined(windows):
proc workerThunkWin(arg: pointer): int32 {.stdcall.} =
# Bind the emulated-TLS slot to this worker's GcThread BEFORE any
# nimcall proc runs — `workerMain` (and anything it calls) may touch
# threadvars in its function prologue, which under emulated TLS would
# deref a nil slot if globalsSlot hadn't been set for this OS thread.
when declared(globalsSlot):
let w = cast[ptr Worker](arg)
threadVarSetValue(globalsSlot, addr(w.gcThread))
workerMain(arg)
result = 0'i32

proc spawnOsWorker(w: ptr Worker) =
var dummy: int32 = 0'i32
let h = createThread(nil, PoolWorkerStackSize.int32, workerThunkWin,
cast[pointer](w), 0'i32, dummy)
if h <= 0:
raise newException(ResourceExhaustedError, "cannot create pool worker")
w.osThread = h

elif defined(genode):
# Genode keeps the legacy 1:1 model until its C++-side runtime adapts.
proc spawnOsWorker(w: ptr Worker) =
raise newException(ResourceExhaustedError,
"virtual-thread pool not implemented for Genode")

else:
proc workerThunkPosix(arg: pointer): pointer {.noconv.} =
# Bind the emulated-TLS slot to this worker's GcThread BEFORE any
# nimcall proc runs — see workerThunkWin for the rationale.
when declared(globalsSlot):
let w = cast[ptr Worker](arg)
threadVarSetValue(globalsSlot, addr(w.gcThread))
workerMain(arg)
result = nil

proc spawnOsWorker(w: ptr Worker) =
var attr: Pthread_attr
discard pthread_attr_init(attr)
discard pthread_attr_setstacksize(attr, PoolWorkerStackSize)
if pthread_create(w.osThread, attr, workerThunkPosix, cast[pointer](w)) != 0:
raise newException(ResourceExhaustedError, "cannot create pool worker")
discard pthread_attr_destroy(attr)

proc spawnWorker(): ptr Worker =
result = cast[ptr Worker](c_malloc(csize_t sizeof(Worker)))
zeroMem(result, sizeof(Worker))
initBinSem(result.wake)
spawnOsWorker(result)

proc workerOsThread*(w: ptr Worker): SysThread {.inline.} =
## Read accessor for `Thread[TArg].sys` plumbing.
w.osThread

# ---------------- public API ----------------

proc poolDispatch*(base: ptr ThreadBase; entry: TaskEntry; closure: pointer) =
## Hand `entry(closure)` to a worker. Demand-grown: pops the idle stack
## first, only spawns a fresh OS thread when the stack is empty.
resetBinSem(base.done)
var w = popIdle()
if w == nil:
w = spawnWorker()
w.entry = entry
w.closure = closure
w.base = base
base.worker = cast[pointer](w)
postBinSem(w.wake)

proc poolWaitDone*(base: ptr ThreadBase) =
## Block until the most recent `poolDispatch` on `base` completes.
waitBinSem(base.done)

{.pop.}
61 changes: 61 additions & 0 deletions lib/std/private/threadtypes.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
include system/inclrtl

import std/private/syslocks

const hasSharedHeap* = defined(boehmgc) or defined(gogc) # don't share heaps; every thread has its own

when defined(windows):
Expand Down Expand Up @@ -161,10 +163,69 @@ type

const hasAllocStack* = defined(zephyr) # maybe freertos too?

# ---------------- Virtual-thread primitives ----------------
#
# BinSem is a reusable binary semaphore — `wait` blocks until `post` has been
# called (or returns immediately if `post` arrived first); on wake it clears
# the flag so the same instance can be cycled across many dispatches.
# Defined here (rather than in threadpool_impl.nim) because `Thread[TArg]`
# embeds a `ThreadBase` that contains one.

type
BinSem* = object
L: SysLock
C: SysCond
signaled: bool

ThreadBase* = object
## Per-virtual-thread control block: done semaphore + currently-assigned
## worker. Embedded in `Thread[TArg]`. `worker` is held as an opaque
## `pointer` because the `Worker` type lives in `threadpool_impl.nim`,
## which is included downstream of this file.
done*: BinSem
worker*: pointer # ptr Worker (opaque here)

proc initBinSem*(s: var BinSem) {.inline.} =
initSysLock(s.L)
initSysCond(s.C)
s.signaled = false

proc deinitBinSem*(s: var BinSem) {.inline.} =
deinitSys(s.L)
deinitSysCond(s.C)

proc postBinSem*(s: var BinSem) =
acquireSys(s.L)
s.signaled = true
signalSysCond(s.C)
releaseSys(s.L)

proc waitBinSem*(s: var BinSem) =
acquireSys(s.L)
while not s.signaled:
waitSysCond(s.C, s.L)
s.signaled = false
releaseSys(s.L)

proc resetBinSem*(s: var BinSem) =
acquireSys(s.L)
s.signaled = false
releaseSys(s.L)

proc initThreadBase*(t: var ThreadBase) {.inline.} =
initBinSem(t.done)
t.worker = nil

type
Thread*[TArg] = object
core*: PGcThread
sys*: SysThread
base*: ptr ThreadBase # heap-allocated so copies of Thread[TArg] (e.g.
# joinThread's by-value parameter) keep pointing
# at the SAME done-semaphore. Allocated by
# createThread; never freed under the current
# design (V control blocks are small and
# typically program-lifetime).
when TArg is void:
dataFn*: proc () {.nimcall, gcsafe.}
else:
Expand Down
Loading
Loading