# Concurrency
Deft provides three concurrency primitives that share a single
per-runtime scheduler. Each is tuned for a different pattern:
| Primitive | Use when |
|---|---|---|
| `deftask` | Run a value-producing computation on a separate thread by default; `@stream` for incremental results. |
| `defworker` | Persistent worker or pool — a file loaded in a long-lived child runtime with full module access (see [defworker](defworker)). |
| `defcoro` + `spawn` | Cooperative long-lived process with its own event loop. |
| `after` / `after-cancel` | One-shot timer. |
`defer` (covered in [Control Flow](control-flow)) provides cleanup on
function exit and composes well with all three.
## `deftask` — Async Value
`deftask` runs its body on a **fresh OS thread** with its own runtime
(the closure's bytecode is deep-copied, and global data values it
references are snapshotted) and returns a **future**. There is no worker
pool — each call spawns a new thread. `await` blocks until the future
resolves and yields its value.
The body must be self-contained: native stdlib functions are available, but
**captured locals and user-defined functions do not cross into the
child runtime**. Use literals, global data, and stdlib functions only — a
local captured from the enclosing function (`$url`, say) will come through
as `nil`.
(For full module access — user-defined functions, imports, persistent
state — use [defworker](defworker).)
# Global data referenced by the body is snapshotted into the child runtime. def api-base "https://api.example.com" def f1 [deftask { [http/get "$api-base/a"] }] def f2 [deftask { [http/get "$api-base/b"] }] set results [await-all @{$f1 $f2}]
`@parallel` is accepted as an explicit marker for the same behavior:
def f [deftask { @parallel; [http/get "$api-base/a"] }]
The mode decorators go at the top of the body — each on its own line,
or separated with `;` (`{ @stream body }`, `{ @stream @buffer-size 128 body }`).
`await` accepts one future — or several: `[await $f1 $f2 ...]` blocks for
each in turn and returns a list of values (in order). For lists of
futures, `await-all` is a `stdext/async` wrapper that resolves each in
turn and skips non-future entries:
from "deft/stdext/async" import await-all
### `@stream` — incremental results
For long-running producers, marking the body with `@stream` yields a
stream of values rather than one final result. The producer runs on its
own OS thread; use `@buffer-size N` to set the stream buffer size
(default 64). See [Stdlib](stdlib) for the `task/stream-*`
family.
def s [deftask { @stream @buffer-size 128 yield 1 yield 2 yield 3 }] set row [task/stream-next $s] while ![nil? $row] { # ... process $row ... set row [task/stream-next $s] } # or just: def rows [task/stream-collect $s]
### Error semantics
How a throw inside the task body surfaces depends on the mode:
- **`deftask` / `@parallel`** — the future resolves with the error string
(e.g. `"boom (line 1)"`), and `await` returns it as the task's value. It
does **not** re-throw at the await site.
- **`@stream`** — the stream simply ends; the error is swallowed.
set v [await $f] if [is? "string" $v] { # task body threw: $v is the error message }
## `defcoro` — Cooperative Coroutine
A coroutine is a long-lived process with its own event loop. Use
`spawn` to start one and `vwait` to drive it.
def state [atom %{ :count 0 }] defcoro worker {|name^string| while ([deref $state]~count < 3) { swap! $state { |s| assoc $s :count ($s~count + 1) } echo "$name tick" yield --wait 500 } } set co [spawn [$worker "alpha"]] [vwait] # drives all coroutines on this runtime
### `yield`
`yield` suspends the coroutine and returns control to the caller. The
optional `--wait ms` flag suspends for at least `ms` milliseconds:
defcoro ticker {|| set n 0 while true { incr n echo "tick $n" yield --wait 1000 } }
### `resume`
`resume` wakes a yielded coroutine. The scheduler calls this for you
inside `vwait`; you rarely invoke it directly.
### `coro-state`
`[coro-state $co]` returns one of `:ready`, `:running`, `:suspended`,
`:done`, or `:failed`.
## `after` — One-Shot Timer
`after ms fn` schedules `fn` to run after `ms` milliseconds. Returns a
timer id you can cancel:
set id [after 5000 { echo "five seconds elapsed" }] after-cancel $id # cancel the pending timer
Timers fire on the runtime's event loop — they don't need a separate
thread.
## `sleep`
`sleep ms` blocks the current coroutine/task for at least `ms`
milliseconds, yielding control to other tasks meanwhile. Inside a
plain script (no scheduler running) it just blocks.
sleep 100
## `defer` — Cleanup on Exit
`defer` works inside async code the same way it works in synchronous
functions: the block runs when the enclosing function exits, however
it exits. This makes it ideal for releasing async resources:
def with-connection {|url^string| set conn [net/connect $url] defer { net/close-conn $conn } # ... use $conn — it'll be closed whether we return normally, # throw, or the task is cancelled ... [do-stuff $conn] }
See [Control Flow](control-flow) for the full `defer` reference.
## Per-Runtime Scheduler
Each runtime has exactly one coroutine scheduler. The `deft` scripting
binary drives one runtime; each TUI app panel runs its own runtime in
its own OS thread (see [Runtimes](../02-Platform/runtimes)).
Consequences:
- Coroutines **on the same runtime** share state directly;
no locks needed.
- `deftask` bodies always run in a **separate child runtime** (the
closure is snapshotted, globals are deep-copied) — pass data in through
the body and get the result back through the future/stream.
- Long-lived runtimes **on different runtimes** must use
`pub/*` (see [Pubsub](../02-Platform/pubsub)) or `rpc` (see
[RPC](../02-Platform/rpc)) to communicate.
## Decision Matrix
| Need | Use |
|---|---|
| Run a slow operation in the background while the foreground continues | `deftask { ... }` + `await` |
| Concurrent map over N inputs | `deftask { ... }` (self-contained bodies) + `await-all` |
| Persistent worker with full module access (user fns, imports, state) | `defworker` + `worker/send` |
| Long-lived background process | `defcoro` + `spawn` + `vwait` |
| One-shot delayed action | `after ms fn` |
| Periodic action | `defcoro` with `yield --wait ms` in a loop |
| Cross-runtime messaging | `pub/publish` + `pub/subscribe` |
| Cross-runtime direct call | `rpc` against `@rpc` functions |
### Why `defworker` over `deftask`?
`deftask` snapshots the closure's bytecode + referenced
globals into a fresh one-shot runtime: the body must be self-contained (no
user-defined function calls, no captured locals), and the worker dies after
one job. `defworker` loads a *file* into a persistent child runtime on its
own OS thread — the file's full module graph is available, `@worker`-marked
defs are its handlers, state persists across jobs, and handler errors
resolve to `%Err` without killing the worker. See
[defworker](defworker) for the full reference.
## Quick Reference
| Form | Purpose |
|---|---|
| `[deftask { ... }]` | Run body on a fresh OS thread, return a future |
| `[deftask { @parallel ... }]` | Explicit form of the default |
| `[deftask { @stream ... }]` / `@buffer-size N` | Stream of incremental results |
| `[defworker "path.dft"]` / `[defworker "path.dft" n]` | Persistent worker / pool (see [defworker](defworker)) |
| `[worker/send $w "name" args...]` | Dispatch a job to a worker; returns a future |
| `[worker/stream $w "name" args...]` | Streaming job; `yield` in the handler pushes onto the returned stream |
| `[await $future]` | Block for a future's value |
| `[await $f1 $f2 ...]` | Block for each future, return a list of values |
| `await-all` (from stdext/async) | Block for a list of futures |
| `defcoro name { ... }` | Declare a coroutine |
| `spawn $coro-or-closure` | Start a coroutine |
| `[vwait]` | Drive all coroutines on this runtime |
| `yield` / `yield --wait ms` | Suspend |
| `[coro-state $co]` | Inspect state |
| `[after ms fn]` / `[after-cancel id]` | One-shot timer |
| `sleep ms` | Block current task |