--- title: "Nim Concurrency: Threads, Channels, and the asyncdispatch/chronos Split (2026)" description: "Verified guidance on Nim threading primitives (std/typedthreads, std/locks, channels), ARC/ORC's shared-heap thread model, taskpools/weave versus the deprecated std/threadpool, and the critical, non-interoperable split between std/asyncdispatch and chronos." language: nim framework: null category: language_concurrency tags: - concurrency - parallelism - thread-safety - async - asyncdispatch - chronos - threads - channels - gcsafe - arc-orc keywords: - nim threads:on default nim 2.0 - nim std/typedthreads createThread Thread[T] - nim channels Chan threading/channels - chronos vs asyncdispatch incompatible Future - nim gcsafe pragma meaning - taskpools vs weave vs threadpool nim last_updated: "2026-07-07" difficulty: advanced version: "Nim >= 2.0 (stable line 2.2, currently 2.2.10, April 2026); chronos >= 4.0" related: - README.md - best_practices.md - common_pitfalls.md - metaprogramming.md - testing.md - interop_ffi.md - ../rust/best_practices.md - ../../core/principles/solid.md search_priority: high status: published --- # Nim Concurrency: Threads, Channels, and the asyncdispatch/chronos Split (2026) ## Introduction / Overview Nim's concurrency story is split along two independent axes that must not be conflated: **parallelism** (raw OS threads, thread pools, and data-parallel libraries, for CPU-bound work spread across cores) and **asynchronous I/O** (single-threaded, cooperatively-scheduled `async`/`await` for high-concurrency I/O-bound work). Both axes carry traps that are unusually easy for a code-generating LLM to fall into, because Nim's defaults changed materially at the Nim 2.0 release (August 2023) and because the async ecosystem is genuinely, permanently split between two incompatible frameworks rather than converging on one. This document is verified against the current (2.2.x) Nim manual, the `nim-lang/threading` and `status-im/nim-taskpools`/`status-im/nim-chronos` repositories, and the Nim 2.0 release notes. Where the ecosystem is still in flux (e.g., exact adoption levels of `taskpools` versus `weave`), this is flagged explicitly rather than presented as settled fact. **When to use this guidance**: Writing any Nim code that spawns a thread, shares data between threads, or uses `async`/`await` - and, most importantly, *before* adding any async-touching dependency to a Nim project, to confirm which async ecosystem it commits you to. ## Core Concepts - **`--threads:on` is the default since Nim 2.0**: Prior to Nim 2.0, multi-threading support was opt-in and had to be requested explicitly with the `--threads:on` compiler flag; the RFC to flip this default (`nim-lang/RFCs#361`) was accepted for the 2.0 release, and the Nim 2.0.2 release notes confirm `--threads:on` as "the default." You may still see `--threads:on` written explicitly in older tutorials, CI scripts, and Nimble tasks - it is harmless but no longer necessary on Nim >= 2.0. `--threads:off` still exists and can be passed explicitly to disable OS-thread support entirely (smaller binaries, no `Thread[T]`/channel support), but this is rare in modern code and several stdlib and third-party modules assume threading is available. - **ARC/ORC use a shared heap across threads, unlike the old `refc` GC**: Under the legacy `--mm:refc` garbage collector (Nim 1.x's default), each thread owned its own independent heap, and passing a `ref` across a thread boundary required deep-copying data into that thread's heap. Under ARC/ORC (the default since 2.0), **all threads share one address space and one heap** - a `ref`, `seq`, or `string` can be *moved* to another thread with no copy at all. This is a genuine improvement for performance, but it removes a safety net: ARC/ORC's reference-count increments/decrements are **not atomic**. Two threads simultaneously holding and mutating the same `ref` will race on its hidden refcount field, corrupting memory - Nim avoids this not by making refcounts atomic (slow) but by requiring that shared data be *moved* (transferring sole ownership) rather than *aliased* (copied while multiple owners remain live) across a thread boundary. - **`Isolated[T]` (`std/isolation`) is the compiler-checked mechanism for safe cross-thread transfer**: `isolate(value)` produces an `Isolated[T]` only when the compiler can prove no other alias to `value`'s subgraph remains reachable; `extract(isolatedValue)` moves the value back out on the receiving side. This is the primitive that channel implementations built for ARC/ORC use to make "send a `ref` to another thread" provably safe rather than merely conventionally safe. - **`{.gcsafe.}` asserts a proc touches no globally-shared GC'd memory unsafely**: A `gcsafe` proc does not read or write any global variable holding GC'd memory (`ref`, `string`, `seq`, closures) - directly, or indirectly via calling a non-`gcsafe` proc - except through the isolation/move discipline above. This is what prevents two threads from racing on a shared global's refcount. `createThread` requires its thread-body proc to satisfy this discipline (enforced via the `{.thread.}` pragma on the proc type). - **Async is a second, orthogonal concurrency model**: `async`/`await` in both `std/asyncdispatch` and `chronos` runs on a **single OS thread** by default, using cooperative scheduling around an event loop (epoll/kqueue/select/IOCP depending on platform) - it gives you high I/O concurrency without OS-thread overhead, but zero CPU parallelism unless combined with `spawn`/`taskpools`/`weave` for the CPU-bound portions of the work. ## Best Practices ### 1. Use `std/typedthreads` for raw OS threads `Thread[T]`/`createThread`/`joinThread`/`joinThreads` live in `std/typedthreads` in current Nim. Historically (pre-2.0, and under older `system`-module-based code you may still see in tutorials), these symbols were available directly from `system` without any import once compiled with `--threads:on`. As the standard library has moved toward a "slimmer" `system` module, explicit import of `std/typedthreads` is the current, forward-compatible form and should always be used in new code. ```nim import std/typedthreads import std/locks var counterLock: Lock sharedCounter: int proc worker(id: int) {.thread.} = for _ in 0 ..< 1000: withLock counterLock: inc sharedCounter initLock(counterLock) var threads: array[4, Thread[int]] for i in 0 ..< threads.len: createThread(threads[i], worker, i) joinThreads(threads) deinitLock(counterLock) echo sharedCounter ``` The proc passed to `createThread` must have the `{.thread.}` pragma and must be `gcsafe` - it cannot close over arbitrary GC'd data from the spawning thread without going through an isolation-safe path (a `ptr`, a channel, or an `Isolated[T]`). **Rationale**: `std/typedthreads` is the lowest common denominator underneath every higher-level library on this page - understanding it directly is necessary to reason correctly about what a pool library or channel abstracts away, and it remains the right tool for a small, fixed number of long-lived dedicated worker threads (e.g., one background thread per hardware sensor, one per persistent outbound connection) where a pool's task-queue abstraction adds nothing. **Why this works well in production**: joining every spawned thread explicitly (`joinThreads`) before the enclosing scope exits guarantees no thread outlives the data it was given a `ptr`/`Isolated[T]` reference into - the same structured-lifetime discipline that `taskpools`' `shutdown()` and Rust's `JoinSet` (see [Rust Best Practices](../rust/best_practices.md#4-structure-async-code-around-tokios-task-model)) enforce in their own ecosystems. ### 2. Use `std/locks` for mutual exclusion, and prefer `withLock` over manual `acquire`/`release` **Rationale**: `Lock` (a thin wrapper over the OS mutex), `initLock`/`deinitLock`, `acquire`/`release`, and the `withLock(lock): body` template are Nim's primitive synchronization tools. `withLock` releases the lock on the template's `finally`-equivalent path, so an exception raised inside the protected body cannot leave the lock permanently held - a manual `acquire`/`release` pair without a `try/finally` around it can. There is no built-in condition variable beyond `Cond`, which pairs with a `Lock` for wait/signal/notify patterns when a thread must block until another thread changes some shared condition rather than merely mutual-exclude access to it. ```nim import std/locks var queueLock: Lock queueNotEmpty: Cond pending: seq[int] initLock(queueLock) initCond(queueNotEmpty) proc pushWork(item: int) = withLock queueLock: pending.add(item) signal(queueNotEmpty) proc popWork(): int = withLock queueLock: while pending.len == 0: wait(queueNotEmpty, queueLock) result = pending.pop() ``` **Why this works well in production**: `wait(cond, lock)` atomically releases the lock while blocking and reacquires it before returning, which is the standard, deadlock-free way to implement a bounded producer/consumer queue without busy-waiting. ### 3. Choose the correct channel type for the ARC/ORC era **Rationale**: Nim's channel story has **two live implementations** and an LLM must not conflate them - picking the wrong one, or assuming the built-in type has the newer type's isolation guarantees, is a common source of subtly wrong generated code. 1. **The built-in `Channel[T]`**, part of the `system` module's threading support (compiled in whenever `--threads:on` is active; historically documented as `system/channels_builtin`). It provides `open`/`close`, blocking `send`/`recv`, and non-blocking `trySend`/`tryRecv`, bounded by an optional `maxItems` (0 = unbounded). Its own documentation notes it is **unstable when combined with `spawn`/`std/threadpool`**, and a `Channel` instance itself cannot be copied or passed by value between threads - you must share it via a `ptr` or a `global`. ```nim import std/typedthreads var chan: Channel[int] chan.open() proc producer() {.thread.} = for i in 1 .. 5: chan.send(i) var t: Thread[void] createThread(t, producer) for _ in 1 .. 5: echo chan.recv() joinThread(t) chan.close() ``` 2. **`threading/channels`** (`Chan[T]`), from the separate, actively developed `nim-lang/threading` Nimble package. This is the modern, ARC/ORC-native multi-producer/multi-consumer channel, designed from the outset to move `Isolated[T]` data safely, rather than relying on the older `system` channel's implicit conventions. It requires adding `threading` as a Nimble dependency - it is **not** part of the bundled standard library. ```nim # nimble install threading import threading/channels import std/typedthreads import std/isolation var chan = newChan[string]() proc producer() {.thread.} = for msg in ["one", "two", "three"]: chan.send(isolate(msg)) var t: Thread[void] createThread(t, producer) for _ in 1 .. 3: var received: string chan.recv(received) echo received joinThread(t) ``` **Verification note**: exact `Chan[T]` API signatures (`newChan`, `send`, `recv`, `trySend`, `tryRecv`) come from the `nim-lang/threading` GitHub repository's `threading/channels` module documentation; treat minor signature details (e.g., whether `send` takes the value directly or requires an explicit `isolate()` call at the call site) as subject to change across `threading` package releases, and check the installed version's own generated docs (`nimble path threading`) before relying on exact argument order in production code. **Guidance**: for new code on Nim >= 2.0, prefer `threading/channels`' `Chan[T]` when the project can take the extra Nimble dependency; fall back to the built-in `Channel[T]` only for small scripts where avoiding an extra dependency matters more than the newer type's ARC/ORC-native isolation guarantees. ### 4. Prefer `taskpools`/`weave` over the deprecated `std/threadpool` for pooled task parallelism **Rationale**: `std/threadpool`'s `spawn`/`sync` API suffered from global-queue contention and no work-stealing; independent benchmarks cited in its own deprecation discussion showed programs compiled with `--threads:on` running roughly 40% slower even at a single thread of concurrency due to the module's inherent overhead. Its own deprecation message explicitly redirects users to `malebolgia`, `taskpools`, or `weave`. | Library | Status | Model | Use when | |---|---|---|---| | `std/threadpool` (`spawn`/`sync`) | **Deprecated.** The module's own deprecation message directs users to `malebolgia`, `taskpools`, or `weave` instead. | Global work queue, no work-stealing | Never for new code - kept only for legacy compatibility | | `taskpools` (`status-im/nim-taskpools`) | Actively maintained, lightweight, ARC/ORC-native | Work-stealing thread pool; `Taskpool.new(numThreads)`, `tp.spawn expr` returns a `Flowvar[T]`, `sync(flowvar)` blocks for the result, `syncAll()`/`shutdown()` | Default choice for general task-parallel workloads (fire off N independent units of work, collect results) | | `weave` (`mratsim/weave`) | Actively maintained, HPC-oriented | Nestable data-parallelism and task-parallelism runtime with a work-stealing scheduler, designed for fine-grained tasks (matrix/tensor operations, tree/graph traversal) at large core counts | Numerically intensive, deeply nested parallel workloads where `taskpools`' simpler model is insufficient | | `malebolgia` | Newer, lighter-weight structured-concurrency-style library, mentioned directly in `threadpool`'s own deprecation message as an alternative | Structured spawn/await over a thread pool | Verify current maturity directly against its repository before adopting - it is referenced here because it is the stdlib's own suggested replacement, not because this document has exhaustively audited its API stability | ```nim # taskpools: the general-purpose default import taskpools proc computeChunk(id: int): int = id * id var tp = Taskpool.new(numThreads = 4) var results: array[4, Flowvar[int]] for i in 0 ..< 4: results[i] = tp.spawn computeChunk(i) for i in 0 ..< 4: echo sync(results[i]) tp.shutdown() ``` **Raw threads vs. a pool library**: reach for `std/typedthreads` directly only when you need long-lived, independently-scheduled workers (e.g., a dedicated background thread pumping a queue for the process's lifetime). For "run N independent units of work and collect results," a pool (`taskpools` first, `weave` for HPC-shaped workloads) removes the boilerplate of manual thread creation/joining and gives you work-stealing load balancing for free. ### 5. Pick exactly one async ecosystem per project - `std/asyncdispatch` or `chronos`, never both This is the single highest-value piece of guidance in this document. Nim has **two actively used, mutually incompatible async ecosystems**, and mixing them in one codebase does not fail gracefully. **`std/asyncdispatch`**: Bundled with every Nim installation. Provides `Future[T]`, the `async` macro (transforms a proc into a state machine driven by a single-threaded event loop), `await`, and `waitFor`. Its event loop dispatches via the platform's native I/O multiplexer (epoll on Linux, kqueue on BSD/macOS, IOCP on Windows). It is the foundation for stdlib modules `std/asyncnet`, `std/asynchttpserver`, and the async variant of `std/httpclient`, and for community libraries and frameworks that predate or intentionally avoid the chronos dependency (e.g., `jester`, which can also run synchronously via `httpbeast`). **`chronos`**: A separate package (`status-im/nim-chronos`), not part of the standard library, actively maintained by the Status/Nimbus (Ethereum client) organization. Verified, documented differences from `asyncdispatch`: - **Its own `Future[T]` type** - a distinct generic type from a different module, not a re-export or subtype of `asyncdispatch.Future[T]`. - **A unified callback signature** (`CallbackFunc = proc (arg: pointer = nil)`), versus `asyncdispatch`'s several distinct callback shapes (`proc()` for `callSoon`, `proc(fut: Future[T])` for completion handlers). - **Forward-order callback scheduling**, versus `asyncdispatch`'s reverse-order processing of completion callbacks - code that depends on callback ordering behaves differently under the two frameworks even where the surface syntax looks identical. - **Exception-effect (`{.raises.}`) tracking integrated into its `async` macro**, which `asyncdispatch` does not provide. - **Its own I/O stack**: transports, an HTTP server/client with built-in TLS (no OpenSSL dependency required), WebSocket support - none of which route through `asyncdispatch`'s event loop. - **Origin**: chronos grew out of dissatisfaction with `asyncdispatch`'s behavior and performance under Status's demanding peer-to-peer networking workloads (documented in the long-running `nim-lang/RFCs` discussion on "Multiple async implementations"); by its v4 release it had diverged far enough (own `Future` type, its own exception-tracking model) that it is now correctly understood as a second, independent async framework rather than a drop-in-compatible fork. Notable chronos-dependent projects/libraries: `libp2p` (the peer-to-peer networking stack used by Nimbus and other Ethereum clients), `presto` (a REST API framework), `nim-websock`, and `scorper` (a chronos-based HTTP framework offered as an alternative to `asyncdispatch`-based web stacks). **Why mixing them fails, concretely**: `Future[T]` from `chronos` and `Future[T]` from `std/asyncdispatch` are two distinct generic types the Nim compiler has no relationship between. If you write an `async proc` using `import asyncdispatch` and call into a library whose own `async proc`s are built on `import chronos`, you will get one of two symptoms, never a clean bridge: - **A compile-time type mismatch** when the two `Future[T]` types meet at a call boundary - e.g. attempting to `await` a chronos-returned `Future[string]` inside an `asyncdispatch`-flavored `async proc`, or passing one where the other is expected, produces an ordinary "type mismatch" error citing two apparently identical-looking but module-distinct `Future[string]` types. - **A silent hang**, in the rarer case where generic code accepts either `Future[T]` structurally (e.g., inside a template) - because each framework runs its own independent global event loop/dispatcher, a `Future` created by chronos's I/O primitives will never be driven to completion by `asyncdispatch`'s `waitFor`/`poll()`, and vice versa. The program blocks forever waiting on a future that the running loop has no way to advance. There is no supported compatibility shim as of this writing; a proposed pattern (a compile-time flag such as `-d:chronosAsync` for libraries wanting to support both) has been discussed in the ecosystem but is not a settled, universally adopted standard - verify the current state directly against the library in question rather than assuming such a flag exists. **The guidance, concretely**: 1. Decide up front: `std/asyncdispatch` (zero extra dependency, adequate for most application-level I/O) or `chronos` (required if you depend on `libp2p`, `presto`, or anything in the Status/Nimbus ecosystem, or if you need its stronger effect-tracking and I/O feature set). 2. Before adding any Nimble dependency that does networking, HTTP, or async I/O, check which framework its own `async proc`s are built on - Nimble does not model or enforce this constraint, and it is entirely possible to `nimble install` two libraries that silently commit you to both ecosystems. 3. Never `import asyncdispatch` and `import chronos` (or a library built on the other) in the same binary's async call graph. Isolating a legacy component that used the other stack behind a synchronous (blocking) interface, run in its own thread, is the only currently workable interop pattern if a hard migration is not feasible. ### 6. Treat `{.gcsafe.}` as a real safety proof, not a formality to silence a warning **Rationale**: `{.gcsafe.}` (inferred automatically for many procs, or asserted explicitly) is the compiler's guarantee that a proc does not read or write any global holding GC'd memory - directly or via a call chain - in a way that could race with another thread's access to that same global. It exists specifically because ARC/ORC's refcount operations are not atomic; two threads incrementing/decrementing the same object's hidden refcount concurrently is a memory-corrupting race, not merely a logic bug. ```nim import std/typedthreads var sharedLog: seq[string] = @[] proc unsafeAppend(msg: string) = sharedLog.add(msg) # touches a GC'd global -- NOT gcsafe proc worker() {.thread.} = # The following line fails to compile: unsafeAppend is not gcsafe, # and the compiler will not silently let a thread proc call it. # unsafeAppend("from worker") discard ``` The correct fix is never to reach for `{.cast(gcsafe).}` to silence this without first restructuring the data flow - route the message through a channel (`Chan[string]`) or a `Lock`-protected global instead, so the compiler's rejection reflects a real, now-resolved hazard rather than a suppressed one. **Why this works well in production**: a `gcsafe` violation caught at compile time is a five-minute fix (add a channel or a lock); the same bug, if bypassed with an unchecked cast, becomes a heisenbug that reproduces only under load, on specific hardware, or under a thread sanitizer - exactly the class of defect this pragma exists to make impossible to ship silently. ## Common Pitfalls & Anti-Patterns ### Pitfall: Mixing `asyncdispatch` and `chronos` libraries in one project **Problem**: A dependency graph that pulls in one library built on `std/asyncdispatch` and another built on `chronos` (common when adding a "just works" HTTP client or web framework without checking its async foundation) produces the type-mismatch/hang failure modes described above, often deep inside generated or vendored code where the root cause is not obvious from the error message alone. **Recommended approach**: Audit the async foundation of every networking/HTTP/timer dependency before adding it (`grep -r "import chronos\|import asyncdispatch"` across a candidate library's source, or check its `.nimble` `requires` line for a `chronos` dependency). Standardize on one framework at the project level and document the choice. ### Pitfall: Assuming `--threads:on` still needs to be added manually **Problem**: Older tutorials, blog posts, and copy-pasted `nim.cfg`/`config.nims` snippets emphasize adding `--threads:on`. On Nim >= 2.0 this is redundant (it is already the default) - worse, an LLM that treats this flag as the *load-bearing* fix for a threading problem will miss the actual, current-generation issue (usually a `gcsafe` violation or an unsafely shared `ref`) while confidently "fixing" a non-problem. **Recommended approach**: Do not add `--threads:on` to new Nim >= 2.0 projects (harmless if present, but treat its absence from a diagnosis as a non-issue); if `--threads:off` is explicitly set in an inherited config file and thread code fails to compile, remove that override rather than assuming threading needs re-enabling elsewhere. ### Pitfall: Passing a non-`gcsafe`-safe closure or shared `ref` across a raw thread boundary **Problem**: Capturing a GC'd global (`ref`, `seq`, `string`, or a closure over one) in a proc passed to `createThread` without going through a channel, `ptr`, or `Isolated[T]` either fails to compile (`gcsafe` violation) or - if the check is bypassed with `{.cast(gcsafe).}` without actually ensuring exclusive ownership - compiles into a genuine data race on ARC/ORC's non-atomic refcounts, corrupting memory non-deterministically. **Recommended approach**: Move data across the boundary via `Isolated[T]`/a `Chan[T]` channel, or share only `ptr`-typed data explicitly allocated outside the GC (e.g., `alloc`/`createShared`) and manage its lifetime manually. Treat `{.cast(gcsafe).}` exactly like `unsafe` blocks in other languages - an escape hatch that shifts the safety proof onto the author, not a fix. ## Testing Strategies - Test the pure, single-threaded logic of a worker function directly, without spinning up threads, for correctness of the business logic itself. - For genuine concurrency-correctness tests (race conditions, deadlocks), prefer a small number of stress tests that run many iterations under `ThreadSanitizer` (`--passC:-fsanitize=thread --passL:-fsanitize=thread`, GCC/Clang backend) rather than relying on timing-based assertions alone - Nim's own issue tracker documents cases where `withLock` failed to prevent a race that only `ThreadSanitizer` caught. - For async code, test each ecosystem's constructs with its own idioms: `waitFor` an `async proc` under test for `asyncdispatch`; chronos ships its own `unittest`-integrated async test helpers - do not attempt to write a single test helper generic over both `Future[T]` types, for the same reason production code cannot mix them. - See [Nim Testing Guide](testing.md) for `std/unittest` fundamentals that apply regardless of which concurrency model is under test. ## Performance, Security & Scalability Considerations - **Thread creation cost**: OS threads are comparatively expensive to create relative to async tasks; prefer a long-lived pool (`taskpools`) over ad hoc `createThread` calls in hot paths. - **Channel backpressure**: both `Channel[T]` and `Chan[T]` support a bounded capacity; an unbounded channel accepting producer output faster than consumers drain it is a memory-exhaustion vector under load, exactly as with unbounded queues in any language. - **Async event-loop starvation**: a single blocking (synchronous, CPU-heavy) call inside an `async proc` - under either `asyncdispatch` or `chronos` - stalls the entire single-threaded event loop, exactly as in Python's `asyncio`; offload such work to a thread (`taskpools`/raw thread) rather than calling it inline. - **Refcount races are memory-safety bugs, not just logic bugs**: because ARC/ORC's refcounts are non-atomic, an unsynchronized shared-`ref` race under threading is not merely "wrong output" - it can corrupt the heap, unlike a typical unsynchronized-counter race in a tracing-GC language. ## Edge Cases & Advanced Usage - **Combining async and raw threads**: a common, supported pattern is to run an `asyncdispatch`- or `chronos`-based event loop on the main thread while offloading CPU-bound chunks to a `taskpools` pool, then feeding results back via a channel or a `Future` completed from the worker thread - verify the chosen async framework's documented thread-safety guarantees for completing a `Future` from a non-owning thread before relying on this pattern, as this is one of the more fragile interop points even within a single framework. - **`--threads:off` niches**: a small binary that is provably single-threaded (a CLI tool, a WASM-style embedded script) may still deliberately compile with `--threads:off` for a smaller, simpler build; this remains a supported, if uncommon, choice. - **Cross-compiling with threading**: some minimal/embedded C target environments lack a pthreads-equivalent; confirm platform thread-library availability before assuming `std/typedthreads` works unmodified on an exotic target. ## When *Not* to Use / Alternatives | Dimension | Raw threads (`std/typedthreads`) | `taskpools` | `weave` | `asyncdispatch`/`chronos` | |---|---|---|---|---| | Best for | Long-lived dedicated workers | General task-parallel CPU work | Fine-grained HPC/data-parallel workloads | I/O-bound concurrency (network, disk, timers) | | CPU parallelism | Yes (manual) | Yes (work-stealing) | Yes (work-stealing, nestable) | No (single-threaded loop) | | Setup complexity | Highest (manual join/lock management) | Low | Moderate-to-high (HPC-oriented API) | Low, but ecosystem-choice-sensitive | | Ecosystem maturity (2026) | Stable (stdlib) | Actively maintained, growing adoption | Actively maintained, HPC-niche | Both actively maintained; **not interoperable with each other** | **Choose plain synchronous, single-threaded code** when the workload is neither I/O-bound at scale nor CPU-bound enough to justify threading's complexity - Nim's baseline performance without any concurrency is already close to C. ## Related Documentation - [Nim Language Overview & Ecosystem](README.md) - [Nim Best Practices](best_practices.md) - [Nim Common Pitfalls & Anti-Patterns](common_pitfalls.md) - [Nim Metaprogramming](metaprogramming.md) - [Nim Testing Guide](testing.md) - [Nim FFI & Interop](interop_ffi.md) - [Rust Best Practices](../rust/best_practices.md) - Tokio's single-runtime model as a useful contrast to Nim's split async ecosystem - [SOLID Principles](../../core/principles/solid.md) ## Glossary - **ARC/ORC**: Nim's default (since 2.0) deterministic, destructor-based memory management; ORC adds a cycle collector on top of ARC. Both use a shared heap across threads. - **`gcsafe`**: A pragma/effect asserting a proc does not unsafely touch globally-shared GC'd memory across threads. - **`Isolated[T]`**: A compiler-checked wrapper (`std/isolation`) proving a value has no remaining aliases, safe to move across a thread boundary. - **`Channel[T]` / `Chan[T]`**: The built-in `system`-based channel type and the newer `threading/channels` MPMC channel type, respectively. - **`Future[T]`**: The core async handle type; **distinct and incompatible** between `std/asyncdispatch` and `chronos`. - **Work-stealing**: A scheduling strategy (used by `taskpools` and `weave`) where idle worker threads pull tasks from busy peers' queues, improving load balance over a single global queue. --- **Maintenance note**: When updating this document, re-verify the `threading/channels` API surface and `taskpools`/`weave`/`malebolgia` adoption levels against their current repositories, and re-check whether any asyncdispatch/chronos interop shim has been standardized since this was written.