Async / Await
Cold async functions, Future values, await, and structured concurrency.
Zynx async is cold and future-based. Calling an async fn evaluates its
arguments immediately and builds a std.future.Future<T>, but the async body
does not run until the future is consumed by await or by a structured future
API.
import std.future;
import std.os;
async fn load() -> i32 {
return 42;
}
async fn main() {
let work: future.Future<i32> = load();
let value = await work;
_ = os.write("{value}\n");
}
await consumes the future, starts it if needed on the current runtime, drives
it to completion, and moves out the result. A future cannot be awaited twice.
This is ordinary move checking; see
Move, Copy, and Clone.
Creating several futures first does not start them concurrently. They start when awaited, or when a structured future API starts them.
import std.os;
async fn work(label: str, value: i32) -> i32 {
_ = os.write("{label}:run\n");
return value;
}
async fn main() {
let a = work("a", 1);
let b = work("b", 2);
let bv = await b;
let av = await a;
_ = os.write("{av}-{bv}\n");
}
await async_fn(...) is sequential: only that future starts.
let left = await add(20, 1);
let right = await add(10, 11);
await is valid only inside an async fn. It accepts a future.Future<T> or a
scoped future.Task<T> returned by structured APIs.
let value = await load();
let work: future.Future<i32> = load();
let same_value = await work;
For Future<void>, use a bare await or discard assignment:
import std.future;
import std.time;
async fn main() {
await future.sleep(time.ns(0));
_ = await future.yield();
await future.sleep(try time.ms(1));
}
Binding the result of await Future<void> to a named variable is a diagnostic,
because void is not a value.
Bare async calls are rejected. A stored Future<T> must be consumed by await,
future.all, future.race, future.timeout, future.Group.add, or explicit
std.drop(future_value) before it leaves scope. Dropping a cold future drops its
captured values without running the async body.
future.all, future.race, and future.timeout are cold structured futures.
When awaited, they start their child futures in one structured scope.
| Item | Purpose |
|---|---|
future.all(futures...) | Wait for all children, cancelling unfinished siblings after the first failure. |
future.race(futures...) | Return the first observed homogeneous child result and cancel the losers. |
future.timeout(future, duration) | Return the child result or future.AsyncError.Timeout. |
Structured APIs that can cancel children include future.AsyncError in their
returned error set.
import std.future;
async fn first() -> i32 {
return 10;
}
async fn second() -> i32 {
return 32;
}
async fn main() {
let pair = (await future.all(first(), second())) catch { _ => (0, 0) };
assert pair[0] + pair[1] == 42;
}
Structured result selection is deterministic. If multiple child outcomes are observable in the same scan, the lower child slot wins.
Current async semantics specify cold start, result propagation, cancellation, and exactly-once cleanup behavior. They do not guarantee fairness, starvation freedom, cross-runtime ordering, exact poll counts, exact timer or file-descriptor polling counts, or a particular batching strategy.
future.yield() is a cooperative suspension point. It gives the current
runtime a chance to poll other ready work, but it is not a fairness contract.
future.Group<T, E> is the scoped structured-concurrency result stream. E
must include future.AsyncError. A group is created with with, receives
children through group.add, and produces scoped task handles through
group.next().
import std.future;
async fn first() -> i32 {
return 10;
}
async fn second() -> i32 {
return 32;
}
async fn main() {
with future.Group<i32, future.AsyncError>() as group {
group.add(first());
group.add(second());
let a = try await group.next();
let b = try await group.next();
assert a != null;
assert b != null;
}
}
group.next() returns a scoped future.Task<T? throws(E)>. It can be awaited
directly or bound to a scoped local handle inside the declaring group block. It
cannot escape the group block. Use group.cancel_all() to request cancellation
for unfinished children.
future.sleep(duration) and future.timeout(future, duration) use
std.time.Duration.
import std.future;
import std.time;
async fn main() {
await future.sleep(time.ns(0));
await future.sleep(try time.ms(1));
}
future.readable(fd) and future.writable(fd) wait for OS readiness only.
They do not perform I/O; the following std.os.read or std.os.write observes
the real OS result.
Direct-await reference captures are allowed because the future is consumed by
the await expression and cannot outlive the source scope:
try await read_ref(&value);
await mutate_ref(&value);
Stored or structured futures that capture references or raw pointers are rejected in current source.
The current async surface has no public unstructured background API. Use await,
future.all, future.race, future.timeout, or future.Group<T, E> for
structured async work.