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¶
asyncandawaitare keywords after an import whose path begins withstd.async, such asimport std.asyncorfrom std.async.time import sleep; see Soft keywords.from std.async import NAMEimports a name that the root module re-exports; the names are listed understd.asyncre-exports.std.async.preludeis the root module under another path:from std.async.prelude import NAMEimports the same names asfrom std.async import NAME, andimport std.async.preludebindspreludeto 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,
breakorcontinueable 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.