Turmeric's tur/taskgroup and tur/future modules provide structured
concurrency: spawn tasks into a scoped group, wait for all of them to
complete, and cancel them together when something goes wrong.
TaskGroup (stdlib/taskgroup.tur) is a scoped container for fibers.
Tasks are spawned into a group, and the group is not done until every task
has completed. When a task fails or the group is explicitly cancelled, all
running tasks receive a cancellation signal and are expected to cooperate
by checking fiber-cancelled? and exiting promptly.
Future/Promise (stdlib/future.tur) is a one-shot producer/consumer
pair. A Promise is the write end (fulfill or fail); a Future is the
read end (block until settled, or poll non-blockingly). async/await
syntax builds on futures; this guide covers the lower-level API.
| Function | Type | Description |
|---|---|---|
task-group-new |
[] -> TaskGroup |
Create an empty task group |
task-group-spawn |
[^borrow g, f : ptr<void>] -> TaskHandle |
Spawn a fiber into the group |
task-group-join |
[^borrow g, handle : TaskHandle] -> nil |
Wait for a specific task |
task-group-wait |
[^borrow g] -> nil |
Block until all tasks complete |
task-group-cancel |
[^borrow g] -> nil |
Cooperatively cancel all tasks |
task-group-cancelled? |
[^borrow g] -> bool |
Check if the group is cancelled |
task-group-done? |
[^borrow g] -> bool |
Check if all tasks are done |
task-group-cancel-reason |
[^borrow g] -> int |
Why the group was cancelled |
task-group-free |
[g : TaskGroup] -> nil |
Destroy the group |
TaskGroup is :linear: it owns a heap-allocated TaskGroupBlock
(mutex + condvar) that must be freed exactly once with task-group-free.
All accessors declare their group parameter ^borrow, so a group may be
spawned into, waited on, and cancelled any number of times without
consuming it; only task-group-free consumes it.
TaskHandle is a plain opaque handle (not linear): a group hands out
many task handles (one per spawn) that are not individually freed.
(defn worker [n : int] : nil
(println n)
(task-group-task-done my-group))
(def my-group : TaskGroup (task-group-new))
(defn run-workers [] : nil
(task-group-spawn my-group (fn [] (worker 1)))
(task-group-spawn my-group (fn [] (worker 2)))
(task-group-spawn my-group (fn [] (worker 3)))
(task-group-wait my-group)
(task-group-free my-group))
Each spawned task must call task-group-task-done when it finishes
(normally or via cancellation) so the group knows it has completed. When
all tasks have called task-group-task-done, the group is marked done
and task-group-wait returns.
Cancellation is cooperative. task-group-cancel sets the group's
cancelled flag and the thread-local cancelled flag for fibers in the
group. Tasks must periodically check fiber-cancelled? or
task-group-should-exit? and exit if cancelled:
(defn long-running [] : nil
(while (not (fiber-cancelled?))
(do-work)
(if (task-group-should-exit? my-group)
(do (task-group-task-done my-group) (return))
nil))
(task-group-task-done my-group))
task-group-cancel-with-reason sets a reason code alongside the
cancellation. Convenience wrappers cover the common cases:
| Function | Reason code | Meaning |
|---|---|---|
task-group-cancel |
0 | Manual cancel |
task-group-cancel-panic |
1 | A child task panicked |
task-group-cancel-timeout |
2 | A timeout fired |
task-group-cancel-error |
3 | An error occurred |
Read the reason with task-group-cancel-reason.
task-group-join waits for a specific spawned task to complete, identified
by the TaskHandle returned by task-group-spawn. This is useful when you
need a task's result before proceeding but do not want to wait for the
entire group:
(let [h (task-group-spawn my-group (fn [] (compute-result)))]
(task-group-join my-group h)
;; task h is done; other tasks may still be running
...)
| Function | Type | Description |
|---|---|---|
promise-new |
[] -> Promise |
Allocate an unsettled shared cell |
future-handle |
[^borrow p] -> Future |
Borrow a Promise, derive a Future (bumps refcount) |
future-of-cell |
[p : Promise] -> Future |
Transfer a Promise into a Future (consumes p) |
promise-pair |
macro: [cell] -> (Promise Future) |
Expand to both handles from one cell |
promise-fulfill |
[p : Promise, v : int] -> nil |
Settle with a value (consumes p) |
promise-fail |
[p : Promise, e : int] -> nil |
Settle with an error (consumes p) |
future-get |
[^borrow f] -> ptr<void> |
Block until settled; returns a Result |
future-done? |
[^borrow f] -> bool |
Non-blocking check |
future-of |
[v : int] -> Future |
Pre-fulfilled future |
future-free |
[f : Future] -> nil |
Free the future's reference |
promise-free |
[p : Promise] -> nil |
Free the promise's reference |
Promise is :linear (must be settled or freed exactly once). Future
is :affine (may be dropped, but not used after future-free). Both
share a FutureCell with a reference count, so the two aliases can be
freed independently -- whichever drop takes the count to 0 tears the cell
down. The refcount uses atomic operations, so a Promise fulfilled on one
thread and a Future freed on another are race-free.
(let [p (promise-new)
f (future-handle p)]
(promise-fulfill p 42) ; producer settles (consumes p)
(let [result (future-get f)] ; consumer blocks until settled
;; result is a ptr<void> to a Result in canonical tagged layout
(println (ok? result)) ; => true
(println (ok-val result)) ; => 42
(future-free f)))
future-of creates a future that is already settled with a value. It is
immediately done:
(let [f (future-of 99)]
(println (future-done? f)) ; => true
(future-free f))
promise-fail settles the promise with an error code. future-get
returns a Result whose err? is true:
(let [p (promise-new)
f (future-handle p)]
(promise-fail p 1) ; settle with error code 1
(let [result (future-get f)]
(println (err? result)) ; => true
(println (err-val result)) ; => 1
(future-free f)))
A common pattern: spawn tasks into a group, each fulfilling a promise with its result, then wait for the group and collect results from the futures.
;; Spawn N workers, each fulfills a promise with its result
(let [g (task-group-new)
p1 (promise-new)
p2 (promise-new)
f1 (future-handle p1)
f2 (future-handle p2)]
(task-group-spawn g (fn [] (do (promise-fulfill p1 (compute-1)) (task-group-task-done g))))
(task-group-spawn g (fn [] (do (promise-fulfill p2 (compute-2)) (task-group-task-done g))))
(task-group-wait g)
(let [r1 (ok-val (future-get f1))
r2 (ok-val (future-get f2))]
(println (+ r1 r2))
(future-free f1)
(future-free f2)
(task-group-free g)))
The async/await syntax builds on futures. (async body) creates a
fiber that executes body and returns a Future<T>. (await fut)
suspends the current fiber until the future completes. See
async-await-guide.md for the full syntax and
semantics.
:linear and :affine disciplines are
covered in substructural-types-guide.md.