You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add lightweight, Loom-inspired concurrency to jank using C++20 coroutines and ASIO. future launches work onto a global continuation pool and returns a future object; deref / @ suspends the calling coroutine (not the thread) if the value isn't ready yet. A separate I/O pool is provided for blocking I/O work via on-io-pool, and user-created pools are supported for CPU-intensive or custom work via make-pool and on-pool. The top-level is an implicit coroutine, so @ is valid anywhere. At the REPL, every expression expr is implicitly evaluated as @(future expr).
API Surface
;; Launch lightweight work on the continuation pool
(future expr)
;; Dispatch blocking I/O to the I/O pool
(on-io-pool expr)
;; Create a new ASIO thread pool of n threads
(make-pool n)
;; Dispatch work to any pool (built-in or user-created)
(on-pool pool & body)
;; Deref a future; suspends the coroutine, not the thread
@my-future
(deref my-future)
(deref my-future timeout-ms timeout-val)
;; Predicates
(realized? f)
(future? f)
Motivation
Jank currently has no story for async I/O or concurrent work. Clojure's future + deref model is familiar and composable. C++20 coroutines + ASIO let us implement this without a custom scheduler, at low complexity cost.
Design
1. Thread pools
Two built-in ASIO thread pools are initialized at runtime startup, and users may create additional pools with make-pool:
Continuation pool — sized to max(std::thread::hardware_concurrency × 1.5, 4). Used exclusively for coroutine continuations and lightweight dispatch. Blocking on this pool is an error; it must remain free to schedule and resume coroutines at all times.
I/O pool — sized to max(std::thread::hardware_concurrency × 0.5, 2) threads by default. Used exclusively for blocking I/O (file I/O, synchronous network calls, FFI calls into blocking C libraries). Blocking inside this pool is explicitly permitted and expected.
All future continuations run on the continuation pool. All on-io-pool work is dispatched to the I/O pool. on-pool dispatches work to any pool, including user-created ones.
Future work: configurable pool sizes via runtime options.
2. Coroutine task type
A single jank_task<T> coroutine type wraps a shared state (promise + future pair). When a coroutine suspends on a future produced by on-io-pool or on-pool, its continuation is always re-posted to the continuation pool once the value is ready — regardless of which pool produced the value. This is the critical invariant: I/O pool threads and user pool threads never directly execute coroutine continuations. This ensures:
Continuations never run on I/O or user pool threads.
Long-running I/O or CPU work never stalls the continuation pool.
The continuation pool remains exclusively available for scheduling and resumption.
3. future
(future expr)
A macro that posts a coroutine executing expr onto the continuation pool and returns an opaque JankFuture wrapping the shared state. Execution begins eagerly. Use this for lightweight, non-blocking work only.
4. on-io-pool
(on-io-pool expr)
A macro that submits expr to the I/O pool and returns a JankFuture. Blocking inside expr is safe and expected. When the returned future is derefd from a coroutine, the calling coroutine suspends and its continuation is re-posted to the continuation pool once the value is ready — the I/O pool thread never runs coroutine continuations directly.
Use this for any work that would otherwise stall a compute thread: file I/O, synchronous network calls, FFI calls into blocking C libraries, etc.
5. make-pool
(make-pool n)
Returns a new ASIO thread pool with n threads. The returned pool value can be passed to on-pool to dispatch work onto it. This allows callers to create dedicated pools for CPU-intensive computation, special workloads, or any work that should be isolated from the built-in I/O pool.
(defcpu-pool (make-pool4))
6. on-pool
(on-pool custom-pool & body)
A macro that submits body to custom-pool (any pool value, including those created with make-pool) and returns a JankFuture. Long-running or CPU-intensive computation inside body is safe and expected. When the returned future is derefd from a coroutine, the calling coroutine suspends and its continuation is re-posted to the continuation pool once the value is ready — the custom pool thread never runs coroutine continuations directly.
Use this for any CPU-bound computation that would monopolise a continuation thread: heavy numerical work, compression, parsing large inputs, FFI into compute-intensive C libraries, etc.
If the future is already resolved → return the value immediately.
If not → suspend the calling coroutine and register a continuation that resumes it on the continuation pool once the value is ready. The OS thread is not blocked.
If a timeout is supplied, a timer is armed alongside the await. Both the timer and the value completion are serialised through a strand, guaranteeing that exactly one of them fires the continuation and the other is cancelled — there is no race. On timeout, timeout-val is returned.
8. Implicit top-level coroutine
main is wrapped in a top-level coroutine dispatched onto the continuation pool. The runtime blocks until this coroutine completes. Unhandled exceptions escaping the top-level coroutine are caught and reported before the runtime exits; they do not silently disappear.
At the REPL, every expression expr submitted by the user is implicitly evaluated as @(future expr). This means:
The expression is dispatched as a coroutine onto the continuation pool.
The REPL blocks the prompt until the expression resolves, preserving familiar interactive semantics.
@ and other async operations inside the expression work correctly.
Exceptions surface immediately at the REPL prompt rather than being lost.
Future work: defined drain and shutdown behaviour under SIGINT and explicit process exit.
9. Error propagation
Exceptions thrown inside a future, on-io-pool, or on-pool body are caught and stored as jank runtime exceptions in the shared state. On deref, the stored exception is rethrown using jank's standard exception machinery. A failed future rethrows on every deref call, consistent with Clojure's behaviour for failed futures.
10. realized? and future?
realized? is a non-blocking atomic check on the shared state. Returns true once the future has a value or has failed.
future? returns true if and only if its argument is a JankFuture instance.
What we're explicitly not doing
No custom scheduler — ASIO's pools are the scheduler.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
RFC: Loom-style Concurrency for jank
Summary
Add lightweight, Loom-inspired concurrency to jank using C++20 coroutines and ASIO.
futurelaunches work onto a global continuation pool and returns a future object;deref/@suspends the calling coroutine (not the thread) if the value isn't ready yet. A separate I/O pool is provided for blocking I/O work viaon-io-pool, and user-created pools are supported for CPU-intensive or custom work viamake-poolandon-pool. The top-level is an implicit coroutine, so@is valid anywhere. At the REPL, every expressionexpris implicitly evaluated as@(future expr).API Surface
Motivation
Jank currently has no story for async I/O or concurrent work. Clojure's
future+derefmodel is familiar and composable. C++20 coroutines + ASIO let us implement this without a custom scheduler, at low complexity cost.Design
1. Thread pools
Two built-in ASIO thread pools are initialized at runtime startup, and users may create additional pools with
make-pool:max(std::thread::hardware_concurrency × 1.5, 4). Used exclusively for coroutine continuations and lightweight dispatch. Blocking on this pool is an error; it must remain free to schedule and resume coroutines at all times.max(std::thread::hardware_concurrency × 0.5, 2)threads by default. Used exclusively for blocking I/O (file I/O, synchronous network calls, FFI calls into blocking C libraries). Blocking inside this pool is explicitly permitted and expected.All
futurecontinuations run on the continuation pool. Allon-io-poolwork is dispatched to the I/O pool.on-pooldispatches work to any pool, including user-created ones.2. Coroutine task type
A single
jank_task<T>coroutine type wraps a shared state (promise + future pair). When a coroutine suspends on a future produced byon-io-pooloron-pool, its continuation is always re-posted to the continuation pool once the value is ready — regardless of which pool produced the value. This is the critical invariant: I/O pool threads and user pool threads never directly execute coroutine continuations. This ensures:3.
future(future expr)A macro that posts a coroutine executing
expronto the continuation pool and returns an opaqueJankFuturewrapping the shared state. Execution begins eagerly. Use this for lightweight, non-blocking work only.4.
on-io-pool(on-io-pool expr)A macro that submits
exprto the I/O pool and returns aJankFuture. Blocking insideexpris safe and expected. When the returned future isderefd from a coroutine, the calling coroutine suspends and its continuation is re-posted to the continuation pool once the value is ready — the I/O pool thread never runs coroutine continuations directly.Use this for any work that would otherwise stall a compute thread: file I/O, synchronous network calls, FFI calls into blocking C libraries, etc.
5.
make-pool(make-pool n)Returns a new ASIO thread pool with
nthreads. The returned pool value can be passed toon-poolto dispatch work onto it. This allows callers to create dedicated pools for CPU-intensive computation, special workloads, or any work that should be isolated from the built-in I/O pool.6.
on-pool(on-pool custom-pool & body)A macro that submits
bodytocustom-pool(any pool value, including those created withmake-pool) and returns aJankFuture. Long-running or CPU-intensive computation insidebodyis safe and expected. When the returned future isderefd from a coroutine, the calling coroutine suspends and its continuation is re-posted to the continuation pool once the value is ready — the custom pool thread never runs coroutine continuations directly.Use this for any CPU-bound computation that would monopolise a continuation thread: heavy numerical work, compression, parsing large inputs, FFI into compute-intensive C libraries, etc.
7.
deref/@timeout-valis returned.8. Implicit top-level coroutine
mainis wrapped in a top-level coroutine dispatched onto the continuation pool. The runtime blocks until this coroutine completes. Unhandled exceptions escaping the top-level coroutine are caught and reported before the runtime exits; they do not silently disappear.At the REPL, every expression
exprsubmitted by the user is implicitly evaluated as@(future expr). This means:@and other async operations inside the expression work correctly.9. Error propagation
Exceptions thrown inside a
future,on-io-pool, oron-poolbody are caught and stored as jank runtime exceptions in the shared state. Onderef, the stored exception is rethrown using jank's standard exception machinery. A failed future rethrows on everyderefcall, consistent with Clojure's behaviour for failed futures.10.
realized?andfuture?realized?is a non-blocking atomic check on the shared state. Returnstrueonce the future has a value or has failed.future?returnstrueif and only if its argument is aJankFutureinstance.What we're explicitly not doing
future.Beta Was this translation helpful? Give feedback.
All reactions