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

Reuse workers with an executor

A thread pool reuses workers for many jobs. Set its worker count and queue capacity at construction:

def square(value: i64) -> Result[i64, OverflowError[i64]]:
    Ok(value * value)

type PoolJobError = ParallelError[OverflowError[i64]] | FutureError | SubmitError[()] | PoolError

def main() -> Result[(), IoError | PoolJobError | OverflowError[i64]]:
    with ThreadPoolExecutor(2, 4)? as pool:
        first = pool.submit(square, 6).drop_job()?
        second = pool.submit(square, 9).drop_job()?
        print(first.result()??)?
        print(second.result()??)?
        print(pool.map_result(square, range(6))?)?
    Ok(())
36
81
[0, 1, 4, 9, 16, 25]

submit returns an owned Future[Result[i64, OverflowError[i64]]]; result() waits and consumes it. The first ? handles the future wait, and the second propagates the worker’s overflow error. A failed submission returns SubmitError, which owns the unstarted job. drop_job() drops that job, as for thread starts, and leaves SubmitError[()], which still says why submission failed. Submit jobs before waiting for their results to let them overlap. Leaving with waits for accepted jobs and joins every worker, including on early return or propagated error.

map_result returns successful values in input order, or the first worker error in input order. map instead keeps every returned value, including any Result wrappers. It accepts an owned list or an integer range and a named function or owned reusable immutable closure, keeping a bounded window of jobs outstanding. Job and result storage can fail to allocate.

def main() -> Result[(), AllocError | IoError | ParallelError[OverflowError[i64]] | PoolError]:
    with ThreadPoolExecutor(2, 2)? as pool:
        factor = 3
        scale = def [factor](value: i64) -> Result[i64, OverflowError[i64]]:
            Ok(value * factor)
        print(pool.map_result(scale, range(4))?)?
    Ok(())
[0, 3, 6, 9]

Mapping consumes scale and shares its one immutable environment until all jobs finish. Mutable, one-shot, or borrowed closures cannot be shared this way. map_result accepts the same workers when they return Result[T, E].

You can submit a closure to move several values into a job explicitly:

type PoolJobError = FutureError | SubmitError[()] | PoolError

def main() -> Result[(), AllocError | IoError | PoolJobError]:
    with ThreadPoolExecutor(2, 2)? as pool:
        values = [3, 6, 9]?
        future = pool.submit(def once [values]() -> list[i64]: values).drop_job()?
        print(future.result()?)?
    Ok(())
[3, 6, 9]

Pool jobs own their inputs; use spawn when a worker needs to borrow local data. A worker may use channels, following the same habits as a task, but cannot hold an executor or future. Keep submission and future waits on the owning thread to avoid pool dependency deadlocks. Files, native effects and foreign calls follow the same rules as for scoped workers.

Dropping a future leaves its job running; an unclaimed result is cleaned up. A future may also outlive the pool’s scope: shutdown finishes the job, and the handle retains the result until you retrieve or drop it.

Keep the job instead when you want to retry it or run it locally; a submission backpressure lesson shows how.