Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Consuming thread jobs

with spawn(job)? as task: moves a concrete wholly owned zero-argument closure into a worker. Reusable and one-shot closures are accepted; def once permits moving a capture into the result. Borrowed captures and generator frames are excluded. A named start with owned arguments binds them into such a job. The scoped-thread lifetime and join rules still apply.

The start expression has result Result[(), SpawnError[F]], where F is the concrete job type. Its one variant, SpawnError[F].Unavailable(job, code), returns the complete unstarted job and the i32 native code after any native start failure. Returning, matching, dropping, and retrying the error and job allocate nothing. Capture expressions have already run, but the worker has not. ? widens the error into an enclosing sum containing SpawnError[F], which keeps the job. A kept job’s endpoints stay alive through the joins that the ? exit performs, so the live-endpoint join rule counts them. A generic signature names the job as F. A nongeneric one writes its signature, such as SpawnError[OnceClosure[[], list[i64]]], alone or in a sum; ? resolves that member to the job it propagates, as for a returned closure. Each such member names one producer, so start several jobs from one factory function or a generic helper.

Job-free errors

with spawn(job).drop_job()? as task: gives up the job instead. drop_job() converts Result[(), SpawnError[F]] to Result[(), SpawnError[()]]: on failure it drops the job and its captures at the start, and the error keeps its variant and code, SpawnError[()].Unavailable((), code). When applied directly to spawn, the compiler constructs this job-free error in the failed-start block, without materializing an intermediate job-carrying result in the caller frame. On success it changes nothing. One SpawnError[()] member covers every job, so a function can start distinct closures and propagate one precise error:

def main() -> Result[(), IoError | SpawnError[()]]:
    with spawn(def once () -> i64: 1).drop_job()? as first:
        print(first.join())?
    with spawn(def once () -> str: "two").drop_job()? as second:
        print(second.join())?
    Ok(())

Because the job is gone before ? runs its exit’s joins, its endpoints cannot block them. The conversion copies a tag and the code and allocates nothing. drop_job() also applies to a SpawnError[F] value, or a Result carrying SubmitError[F], as for pool submissions. Erasing the error into Failure drops the job too, but loses the error type; use it only when no precise error fits.

After success, only the worker owns the captures. One-shot invocation moves them into the body; reusable invocation borrows them once and then drops the environment. Join transfers the result exactly once; automatic join drops an unclaimed result. Job storage and the ownership handoff require no heap allocation.