Skip to content

std.async

std.async provides task spawning, timeouts, races, channels, and synchronization primitives in the modules std.async.time, std.async.task, std.async.race, std.async.channel and std.async.sync. The root module std.async re-exports the common surface; std.async.prelude names the same module.

A function of this module that takes a task requires the task's result to have the type its signature declares and to satisfy the Send and Static bounds shown there.

Importing std.async

  • async and await are keywords after an import whose path begins with std.async, such as import std.async or from std.async.time import sleep; see Soft keywords.
  • from std.async import NAME imports a name that the root module re-exports; the names are listed under std.async re-exports.
  • std.async.prelude is the root module under another path: from std.async.prelude import NAME imports the same names as from std.async import NAME, and import std.async.prelude binds prelude to the root module.
  • Refused (INCAN-I0001): a name that the named module neither declares nor re-exports.
from std.async import spawn, sleep, JoinHandle         # accepted
from std.async.prelude import spawn, Mutex             # accepted
from std.async.channel import SenderPermit             # accepted
from std.async import SenderPermit                     # refused: std.async does not re-export SenderPermit (INCAN-I0001)

Task arguments

A parameter bounded by RuntimeFuture[T] (task of spawn, timeout, timeout_ms and race_timeout, awaitable of arm) takes a task: a direct call of an async def written as the argument, a JoinHandle[T], or another awaitable value. Any other argument is refused with INCAN-T0115, including a function value, a value of a type that is not awaitable, and a name bound to the result of an async def call.

import std.async
from std.async.task import spawn

async def work() -> int:
    return 41

def compute() -> int:
    return 41

async def main() -> None:
    first = spawn(work())   # accepted
    second = spawn(work)    # refused: a function value is not a task (INCAN-T0115)
    third = spawn(compute)  # refused: a function value is not a task (INCAN-T0115)
    fourth = spawn(41)      # refused: an int is not awaitable (INCAN-T0115)
    pending = work()
    fifth = spawn(pending)  # refused: a name bound to a call's result is not a task (INCAN-T0115)

Cancellation terms

Term Contract
Cancel-safe Cancelling a pending wait does not consume a value or acquire a resource.
Cancel-safe but lossy Cancelling does not complete the operation, but it can discard an owned value or the waiter's queue position.
Durable once spawned The task continues after its handle is dropped. Dropping the handle loses the result but does not cancel the task.

std.async.time

from std.async.time import Duration, TimeoutError, TimeoutJoinOutcome
from std.async.time import sleep, sleep_ms, timeout, timeout_ms, timeout_join, timeout_join_ms

Functions

Signature Contract
async sleep(seconds: float) -> None Suspends for seconds. Negative values, NaN, and either infinity are treated as zero. The wait is cancel-safe.
async sleep_ms(milliseconds: int) -> None Suspends for milliseconds. Negative values are treated as zero. The wait is cancel-safe.
async timeout[T with (Send, Static), TaskFuture with RuntimeFuture[T]](seconds: float, task: TaskFuture) -> Result[T, TimeoutError] Returns Ok(value) when task finishes before the deadline and Err(TimeoutError()) when the deadline expires. Negative values, NaN, and either infinity are treated as zero. Expiry or cancellation drops the supplied future.
async timeout_ms[T with (Send, Static), TaskFuture with RuntimeFuture[T]](milliseconds: int, task: TaskFuture) -> Result[T, TimeoutError] Millisecond form of timeout. Negative durations are treated as zero.
async timeout_join[T with (Send, Static)](seconds: float, handle: JoinHandle[T]) -> TimeoutJoinOutcome[T] Waits for spawned work without aborting it at the deadline. Negative values, NaN, and either infinity are treated as zero. A timeout returns the live handle. Cancelling this wait drops its owned handle and detaches the task.
async timeout_join_ms[T with (Send, Static)](milliseconds: int, handle: JoinHandle[T]) -> TimeoutJoinOutcome[T] Millisecond form of timeout_join. Negative durations are treated as zero.

A finite seconds of 18446744073709551616.0 (264) or more, passed to sleep, timeout, timeout_join or race_timeout, panics.

Duration

pub model Duration:
    pub secs: int
    pub nanos: int

The timing functions take seconds or milliseconds, not a Duration.

Signature Result
Duration(secs: int, nanos: int) Direct field construction. It does not clamp or normalize either field.
Duration.from_secs(secs: int) -> Duration Returns zero for secs <= 0; otherwise returns (secs, 0) exactly.
Duration.from_millis(millis: int) -> Duration Returns zero for millis <= 0; otherwise returns secs = millis // 1000 and nanos = (millis % 1000) * 1_000_000.
Duration.from_secs_f64(secs: float) -> Duration Returns zero for nonpositive inputs and NaN. A positive input gives the fields below. Positive infinity gives secs = 9223372036854775807 and nanos = 9223372036854775807.

The integer constructors are exact for every positive int. Their results satisfy secs >= 0 and 0 <= nanos < 1_000_000_000.

For a positive input, from_secs_f64 sets secs to the input's whole seconds and nanos to (input - float(secs)) * 1_000_000_000, each converted to int and saturated at the int range. For a large positive input either value saturates, and nanos can fall outside 0 <= nanos < 1_000_000_000. Positive infinity is not treated as zero.

TimeoutError

TimeoutError implements Error.

Method Returns
message(self) -> str "operation timed out"
source(self) -> Option[str] None

TimeoutJoinOutcome[T]

Variant Payload Meaning
Completed T The task returned successfully before the deadline.
JoinFailed TaskJoinError The task finished before the deadline but could not be joined.
TimedOut JoinHandle[T] The deadline expired. The task is still running and the returned handle can be awaited or aborted.

std.async.task

from std.async.task import JoinHandle, TaskJoinError, spawn, spawn_blocking, yield_now

Functions

Signature Contract
spawn[T with (Send, Static), TaskFuture with RuntimeFuture[T]](task: TaskFuture) -> JoinHandle[T] Starts an async task. The task is durable once spawned.
spawn_blocking[T with (Send, Static), TaskFn with RuntimeFnOnce[T]](task: TaskFn) -> JoinHandle[T] Schedules a no-argument callable on the blocking pool. Once work starts, it cannot be cancelled.
async yield_now() -> None Yields to the runtime scheduler. The wait is cancel-safe.

JoinHandle[T]

Operation Result
await handle Result[T, TaskJoinError]: Ok(value) when the task returned, Err(error) when it panicked or was cancelled. The handle is used up.
handle.abort() -> None Requests cancellation of async work. For spawn_blocking, it prevents only work that is still queued.
await handle after handle.abort() Err(error) with error.is_cancelled() true, unless the task had already completed; a completed task gives its ordinary result.
Dropping the handle Detaches the task.

JoinHandle[T] implements neither Clone nor Debug. A model or class field or an enum variant payload of this type, directly or inside a list, dict, Option, Result or tuple, is refused (INCAN-T0113; see Automatic derives).

Loops over task handles

A for loop hands on its handles when each item is a JoinHandle[T], or a tuple or collection that contains one, and the loop body uses by value a loop binding that holds a handle: it awaits it, passes it as an argument to a function, method or constructor other than a builtin function, assigns it to another name, returns or yields it, breaks with it, puts it in a new tuple, list or set or as a value in a new dict, or iterates it (a list of handles) in a nested for loop that hands on its items.

What such a loop iterates decides how it receives the handles:

Iterable The loop
A local list binding, a parameter not marked mut, or the variable of an enclosing loop that owns its items (that loop takes its items or receives them by value) Takes the handles out of the list, leaving it empty.
A list literal or comprehension, a Generator, a value iterated through an iteration protocol, or the result of an await, of a call other than enumerate(...) and zip(...), or of a method of a type other than a builtin collection Receives each handle by value.
Anything else, such as a list element, a field, a list, dict or set method like values(), enumerate(...), zip(...), a mut parameter, the variable of a loop that reads its items in place, or a static Refused (INCAN-T0119).

After a loop that takes the handles out of a list binding, these are refused (INCAN-T0119):

  • a read of the list inside the loop body;
  • a read of the list after the loop, until an assignment gives the name a new list on every path after the loop, with no branch, break or continue able to skip it;
  • a closure that captured the list before the loop;
  • an enclosing loop that repeats the loop over a list defined outside it, unless each pass assigns the list a new one before the loop, and, when it does, a read of the list in the enclosing loops, their conditions included, that comes before that assignment.

Why a loop takes the items is explained in Loops that take their items; a worked program is Await every task in a list.

import std.async
from std.async.task import spawn

async def work() -> int:
    return 1

async def main() -> None:
    handles = [spawn(work()), spawn(work())]
    for handle in handles:
        match await handle:          # accepted
            Ok(value) => println(value)
            Err(_) => println("join failed")
    println(len(handles))            # refused: the loop emptied handles (INCAN-T0119)
import std.async
from std.async.task import spawn

async def work() -> int:
    return 1

async def main() -> None:
    mut handles = [spawn(work()), spawn(work())]
    for handle in handles:
        println(len(handles))        # refused: the loop takes the items of handles (INCAN-T0119)
        match await handle:
            Ok(value) => println(value)
            Err(_) => println("join failed")
    handles = [spawn(work())]
    println(len(handles))            # accepted
import std.async
from std.async.task import JoinHandle, spawn

async def work() -> int:
    return 1

def launch() -> list[JoinHandle[int]]:
    return [spawn(work())]

async def main() -> None:
    for handle in launch():                     # accepted: a call result
        match await handle:
            Ok(value) => println(value)
            Err(_) => println("join failed")
    table = {"a": spawn(work())}
    for handle in table.values():               # refused: values() reads the dict in place (INCAN-T0119)
        match await handle:
            Ok(value) => println(value)
            Err(_) => println("join failed")

TaskJoinError

Method Returns
message(self) -> str The runtime's join failure message.
source(self) -> Option[str] None
is_cancelled(self) -> bool Whether cancellation caused the join failure.
is_panic(self) -> bool Whether a panic caused the join failure.

std.async.race

from std.async.race import RaceArm, arm, race, race_timeout
Signature Contract
arm[T with Send, R with (Send, Static), TaskFuture with RuntimeFuture[T], OnWin with RuntimeRaceCallback[T, R]](awaitable: TaskFuture, on_win: OnWin) -> RaceArm[R] Packages one future and a callback. The callback receives the future's value only if this arm wins.
async race[R with (Send, Static)](*arms: RaceArm[R]) -> R Polls arms concurrently and returns the winning callback result. Losing arms are dropped. Ready ties use source order. Called with no arms, it panics.
async race_timeout[T with (Send, Static), TaskFuture with RuntimeFuture[T]](seconds: float, task: TaskFuture) -> Option[T] Returns Some(value) before the deadline or None at the deadline. Negative values, NaN, and either infinity are treated as zero. Expiry or cancellation drops task.

RaceArm[R] is the packaged branch type consumed by race. It implements neither Clone nor Debug; as a model or class field or an enum variant payload it is refused with INCAN-T0113.

std.async.channel

from std.async.channel import channel, unbounded_channel, oneshot
from std.async.channel import Receiver, RecvError, Sender, SenderPermit, SendError
from std.async.channel import OneshotReceiver, OneshotSender

Constructors

Signature Returns Constraints
channel[T](buffer: int) -> Tuple[Sender[T], Receiver[T]] A bounded multi-producer, single-consumer channel. buffer <= 0 is normalized to capacity 1. Sends wait when the buffer is full.
unbounded_channel[T]() -> Tuple[Sender[T], Receiver[T]] An unbounded multi-producer, single-consumer channel. Producers can grow memory without a capacity limit.
oneshot[T]() -> Tuple[OneshotSender[T], OneshotReceiver[T]] A channel that carries at most one value. The sender is consumed by its single send operation.

Each constructor takes T as an explicit type argument, as in channel[str](4), or infers it from the channel's sends, receives or an annotation.

Sender[T]

Sender[T] is cloneable. All clones address the same channel.

Method Result and errors Cancellation
async send(self, value: T) -> Result[None, SendError[T]] Returns SendError with the unsent value when the receiver is closed. Cancel-safe but lossy: cancellation while waiting for capacity drops value.
async reserve(self) -> Result[SenderPermit[T], SendError[None]] Reserves one bounded slot, or returns an error when the receiver is closed. Unbounded senders return an immediately usable permit. Cancellation gives up queue position but owns no message value.
try_send(self, value: T) -> Result[None, SendError[T]] Returns immediately. Fails when a bounded channel is full or the receiver is closed. Synchronous.
is_closed(self) -> bool Whether the receiver side is closed. Synchronous.
clone(self) -> Self Another sender for the same channel. Synchronous.

SenderPermit[T]

Method Result and errors
send(self, value: T) -> Result[None, SendError[T]] Uses the reserved slot. An unbounded permit can still return the value if the receiver closes after reservation. A permit is single-use.

Receiver[T]

Receiver[T] does not implement Clone, and neither do OneshotSender[T] and OneshotReceiver[T]; as a model or class field or an enum variant payload each is refused with INCAN-T0113.

Method Result Cancellation
async recv(self) -> Option[T] Returns the next value, or None after the channel is closed and drained. Cancel-safe; cancellation does not remove a message.
try_recv(self) -> Option[T] Returns an available value immediately, otherwise None. None does not distinguish empty from closed. Synchronous.
close(self) -> bool Prevents further sends and returns true. Returns false, closing nothing, while a pending recv on this receiver holds the receiver state. Synchronous.

One-shot types

Method Result Cancellation
OneshotSender[T].send(self, value: T) -> Result[None, T] Delivers the value, or returns it when the receiver is gone. Synchronous.
async OneshotReceiver[T].recv(self) -> Result[T, RecvError] Returns the value, or RecvError when the sender is dropped first. Cancel-safe; cancellation does not consume the value.

Channel errors

Type Public data and methods
SendError[T] value: T preserves the unsent value. message() -> str returns "channel send failed"; source() -> Option[str] returns None.
RecvError message() -> str returns "channel closed: no more messages"; source() -> Option[str] returns None.

std.async.sync

from std.async.sync import Barrier, Mutex, RwLock, Semaphore
from std.async.sync import MutexGuard, RwLockReadGuard, RwLockWriteGuard, SemaphorePermit
from std.async.sync import SemaphoreAcquireError

Mutex[T] and RwLock[T] require T with Clone; a guard's get returns a clone of the guarded value.

Mutex

Method Result Cancellation
Mutex[T].new(value: T) -> Mutex[T] Creates a cloneable mutex. Synchronous.
async Mutex[T].lock(self) -> MutexGuard[T] Waits for exclusive access. The guard releases the lock when dropped. Cancellation does not acquire the lock, but loses queue position.
Mutex[T].try_lock(self) -> Option[MutexGuard[T]] Returns a guard immediately or None when held. Synchronous.
MutexGuard[T].get(self) -> T Clones the guarded value. Synchronous.
MutexGuard[T].set(self, value: T) -> None Replaces the guarded value. Synchronous.

Read-write lock

Method Result Cancellation
RwLock[T].new(value: T) -> RwLock[T] Creates a cloneable read-write lock. Synchronous.
async RwLock[T].read(self) -> RwLockReadGuard[T] Waits for shared read access. Cancellation loses queue position.
async RwLock[T].write(self) -> RwLockWriteGuard[T] Waits for exclusive write access. Cancellation loses queue position.
RwLock[T].try_read(self) -> Option[RwLockReadGuard[T]] Returns a read guard immediately or None. Synchronous.
RwLock[T].try_write(self) -> Option[RwLockWriteGuard[T]] Returns a write guard immediately or None. Synchronous.
RwLockReadGuard[T].get(self) -> T Clones the guarded value. Synchronous.
RwLockWriteGuard[T].get(self) -> T Clones the guarded value. Synchronous.
RwLockWriteGuard[T].set(self, value: T) -> None Replaces the guarded value. Synchronous.

Semaphore

Method Result Cancellation
Semaphore.new(permits: int) -> Semaphore Creates a cloneable semaphore. Negative values are normalized to zero permits. Synchronous.
async Semaphore.acquire(self) -> Result[SemaphorePermit, SemaphoreAcquireError] Waits for one permit. The permit is returned automatically when dropped. Returns an error if the semaphore closes. Cancellation loses queue position but does not acquire a permit.
Semaphore.try_acquire(self) -> Option[SemaphorePermit] Returns a permit immediately or None. Synchronous.
Semaphore.available_permits(self) -> int The number of unreserved permits at the time of the call. Synchronous.

SemaphoreAcquireError.message() -> str returns "failed to acquire semaphore permit: semaphore closed". Its source() -> Option[str] returns None.

Barrier

Method Result Cancellation
Barrier.new(count: int) -> Barrier Creates a cloneable reusable barrier. Counts less than or equal to zero are normalized to one participant. Synchronous.
async Barrier.wait(self) -> int Waits for the generation to fill and returns a slot in 0..count, unique within the generation. An arrival takes a slot freed by a cancelled wait of the generation, the most recently freed first; otherwise it takes the next slot in arrival order, starting at 0. Cancellation before release withdraws the participant and frees its slot; the remaining participants still need a full active generation.

std.async re-exports

from std.async import sleep, spawn, channel

The root module std.async re-exports:

Module Names
std.async.time sleep, sleep_ms, timeout, timeout_ms, timeout_join, timeout_join_ms, Duration, TimeoutError, TimeoutJoinOutcome
std.async.task spawn, spawn_blocking, yield_now, JoinHandle, TaskJoinError
std.async.channel channel, unbounded_channel, oneshot, Sender, Receiver, OneshotSender, OneshotReceiver, SendError, RecvError
std.async.sync Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, Semaphore, SemaphorePermit, SemaphoreAcquireError, Barrier
std.async.race RaceArm, arm, race, race_timeout

SenderPermit is public in std.async.channel but is not re-exported by std.async.