std/asyncdispatch and std/threadpool: Awaiting a FlowVar Without Stalling the Event Loop
0 reputation · 23 Feb 2023, 18:16 UTC
0 reputation · 23 Feb 2023, 18:16 UTC
I am designing a Nim service where async request handlers (std/asyncdispatch) occasionally need to call a legacy blocking library. The documented escape hatch is std/threadpool's spawn, which returns a FlowVar[T], but a FlowVar cannot be awaited natively inside an async procedure. The options appear to be polling isReady in a loop with sleepAsync, blocking on the FlowVar (which would serialize the event loop), or writing a custom bridge.
Constraints: spawn deep-copies its arguments and requires GC-safe, side-effect-disciplined parameters, so shared mutable state must cross the boundary via channels or locks. Behavior also seems to depend on the memory model (--mm:orc default in Nim 2.x versus --mm:refc) and --threads:on, and I have not found an officially blessed async-to-thread adapter in the standard library. Assume Nim 2.x with asyncdispatch and threadpool; exact version details should be verified against local docs.
Is there a standard or recommended pattern for awaiting a FlowVar inside an async proc without blocking the dispatcher? How should the memory model choice influence the design of data passing across this boundary? When polling isReady, what sleep interval strategy avoids both latency and busy-waiting?
32775 reputation · 23 Feb 2023, 19:00 UTC
As of Nim 2.x there is no officially blessed standard-library adapter that turns a FlowVar[T] into an awaitable Future[T]. std/asyncdispatch and std/threadpool are separate worlds: the dispatcher schedules Futures on one thread, while spawn runs work on pool threads and hands back a FlowVar whose only completion APIs are the blocking get()/sync() and the non-blocking isReady. So the practical, honest answer is: write a small polling bridge yourself. It is a few lines, it is the pattern most Nim services use, and it keeps the event loop free.
Wrap the FlowVar in an async proc that checks isReady and yields to the dispatcher with sleepAsync between checks:
import std/[asyncdispatch, threadpool, times]
proc awaitFlowVar[T](fv: FlowVar[T], intervalMs: int = 10): Future[T] {.async.} =
while not fv.isReady:
await sleepAsync(intervalMs)
return ^fv # or fv.sync(); blocks only after isReady is true
proc handleRequest(x: int) {.async.} =
let fv = spawn legacyWork(x)
let result = await awaitFlowVar(fv)
echo result
Because sleepAsync returns control to the dispatcher, other request handlers keep running while the pool thread executes the legacy call. Never call fv.get() or ^fv before isReady is true — that blocks the dispatcher thread and serializes your entire service.
One caution: this code is written against documented Nim 2.x APIs, but I have not compiled it here. Verify against your local nim doc output, since exact signatures (e.g., the ^ operator on FlowVar) should be confirmed for your compiler version.
The trade-off is simple: shorter intervals reduce added latency, longer intervals reduce wasted wake-ups. In practice:
max(10, n div 4) ms bounds the overhead while keeping wake-ups few.Nim 2.x defaults to --mm:orc; --mm:refc is the older alternative. Either way, --threads:on is required for threadpool. The memory model matters less than the discipline at the boundary:
spawn requires GC-safe expressions and deep-copies its arguments into the worker. Treat everything you pass as immutable input; do not share mutable objects between the async thread and the pool thread.std/channels or explicit locks rather than shared mutable state — under orc, unsynchronized shared mutation is a data race, not a managed situation.spawn; pass a compact descriptor (path, ID, offset range) and let the worker load what it needs.nim c --threads:on --mm:orc -r your_service.nim
To confirm the loop stays responsive, run two concurrent handlers — one awaiting a slow spawn, one answering immediately — and check that the fast handler's latency is unaffected. If you use the polling bridge, watch CPU with top; a healthy bridge idles near zero between wake-ups.
This answer reflects documented Nim 2.x behavior as of my knowledge; the standard library evolves, so check whether your Nim version has added an official async/threadpool bridge before adopting the polling wrapper. One detail that would refine this advice: the typical duration of your blocking calls, since that drives the ideal polling interval.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.