std.task

Standard-library module std.task.

Importuse std.task

Types

struct Group<T>#

A scope that owns detached work (std.task.Group).

spawn hands a future to the runtime, which releases its captures when the work completes. A group is the opt-in for making this scope the owner instead, so that leaving the scope waits for the work it started. Groups are never required: a detached spawn stays legal and needs no group at all.

The field is a Future rather than an integer handle so the collector can reach the group's work without learning anything new: the keeper is traced as an ordinary struct field, its task_roots hold the children, and cancelFutureTree already walks those. An integer would form no edge at all, and a synthetic builtin declaration would join the Channel/Isolate exemption that plan 205 cut every other std type away from.

T is the type its children return. It is a phantom parameter — the one field is the keeper, which is Future<void> whatever the children produce — and exists to relate spawn's argument to what next and for hand back. A group starts with T open and the first spawn fixes it, exactly as a channel's first send fixes its message type.

_keeper: Future<void>

Functions

extern fn new() -> Group#

Creates a new group that owns the work spawned into it.

use std.http_server
use std.task

async fn serve_one(request: http_server.Request) -> int {
  http_server.respond(request, http_server.text(200, "ok"))
  1
}

async fn serve(listener: int) -> int {
  let jobs = task.new()
  defer task.cancel(jobs)
  while true {
    let request = http_server.accept_stream(listener)
    task.spawn(jobs, serve_one(request))
  }
  0
}
extern fn spawn<T>(group: Group<T>, work: Future<T>)#

Starts a future and makes this group its owner.

  • group: Group that will wait for the work.
  • work: Future to start now, exactly as a bare spawn would.

The future starts immediately, as spawn does. The difference is who waits for it: the runtime for a bare spawn, this group for this one.

extern fn next<T>(group: Group<T>) -> Future<Optional<Result<T, Error>>>#

Takes the next child to settle, in completion order.

  • group: Group to pull one result from.

some(result) is the next child to finish, whichever one that was; none means the group has no undelivered child left. A child that failed arrives as err, not as a propagated failure — a stream keeps going after one child fails, which is the whole difference between iterating a group and awaiting a barrier.

for result in group is this function in a loop, and is the spelling to reach for. Pull directly only when the loop shape is in the way.

use std.http
use std.task

async fn fetch(url: string) -> string {
  http.get(url).body
}

fn save(body: string) -> int {
  body.len()
}

async fn fetch_all(urls: [string]) -> int {
  let jobs = task.new()
  defer task.cancel(jobs)
  for url in urls { task.spawn(jobs, fetch(url)) }
  let mut saved = 0
  for result in jobs {
    match result {
      ok(body) => { saved += save(body) },
      err(e) => println("failed: " + e.message),
    }
  }
  saved
}

Results arrive as they finish, so a slow child never holds up one that is already done. Spawning into a group while iterating it extends the same iteration rather than racing it, which is what makes a self-feeding worker pool expressible.

extern fn len(group: Group) -> int#

Returns the number of children that have not settled.

  • group: Group whose outstanding count is returned.
extern fn drain(group: Group) -> Future<void>#

Waits for every child to settle.

  • group: Group to drain.

Use this when the work should finish. A child that never returns — an accept loop, say — will not finish on its own, so cancel is the exit for those.

extern fn cancel(group: Group) -> Future<void>#

Cancels every child, then waits for them to notice.

  • group: Group to cancel.

Cancellation is a request. A child parked on an I/O wait stops at once; one that is running observes it at its next is_cancelled(), so this still waits rather than returning immediately.

The two halves are separable, and usefully so: calling this requests the cancellation, and awaiting the result is what waits. So defer task.cancel(group) tears a scope's work down on every exit path without waiting for it, and defer await task.cancel(group) waits — a deferred statement may suspend since 1.20.0.