future
The runtime half of Cryo's async support. async and await are a compile-time transformation — an async function becomes a state machine, and calling it builds a struct rather than running the body. Nothing happens until something polls it, and this module is what does the polling.
The module is named future because async is a reserved keyword. See Asynchronous Programming for the language side.
| Item | Import |
|---|---|
Future | import std::future::traits; |
Poll<T> | import std::future::poll; |
Context, Waker | import std::future::waker; |
Ready<T> | import std::future::ready; |
Sleep | import std::future::timer; |
Join, Select, Timeout, Futures | import std::future::combinator; |
Executor, JoinHandle | import std::future::executor; |
A plain import std::future; brings in the module.
The core types
type trait Future {
type Output;
poll(mut &this, cx: Context*) -> Poll<This::Output>;
}
type enum Poll<T> { Ready(T); Pending; }
A future is polled with a Context, which carries a Waker. Returning Pending means "not yet — I have arranged to be woken"; returning Ready(v) means the value is available and the future is done.
Waker is built on the same non-capturing-function-pointer plus void* erasure that catch_unwind and thread::spawn use. It carries wake(), wake_by_ref(), clone(), and noop() for a waker that does nothing.
Ready<T>::new(value) is a future that completes immediately — useful for testing and for satisfying a Future bound with a value you already have.
Running futures
An Executor owns a queue of ready tasks and a pool of worker threads that drive them.
import std::future;
mut ex: Executor = Executor::new();
const result: Report = ex.block_on(run_pipeline(config));
| Method | Notes |
|---|---|
static new() | Sizes the worker pool to the hardware thread count. |
static with_threads(n: u32) | Pick the count yourself. |
spawn<F, O>(fut: F) | JoinHandle<O>; workers begin driving it immediately. |
spawn_detached<F, O>(fut: F) | Fire and forget. |
block_on<F, O>(root: F) | Spawns root and blocks the calling thread until it finishes. |
join and block_on never poll on the calling thread — the workers own all polling.
JoinHandle<O> carries join() -> Result<O, JoinError>, detach(), abort(), and is_finished(). Dropping a handle without joining or detaching detaches it: the task runs on and its result is discarded. abort() requests cancellation, and the task is reclaimed — its output never produced — the next time a worker reaches it.
Dropping the Executor signals shutdown, joins the workers, then reclaims any still-queued tasks.
Concurrent-poll safety
With two or more workers a task can be woken — by itself mid-poll, or later by another thread — while a worker is already polling it. Polling one future from two workers at once is a data race, so each task carries an atomic run state (IDLE, SCHEDULED, RUNNING, NOTIFIED) that guarantees a task is enqueued at most once and polled by at most one worker.
wake() moves IDLE → SCHEDULED and enqueues, or RUNNING → NOTIFIED and defers to the running worker. A worker moves SCHEDULED → RUNNING to poll, then RUNNING → IDLE on a quiet Pending, or re-schedules on NOTIFIED.
Task isolation
Under --panic=unwind the poll boundary runs inside a catch: a panicking task is caught, finishes as panicked, and its join returns Err(Panicked) — its siblings keep running.
Under the default --panic=abort the catch machinery is a compile error, so the boundary is a plain direct call and a task panic aborts the whole process. That is the accepted degradation; see catch_unwind.
Combinators
Join, Select, and Timeout are ordinary futures that own the futures they compose and drive them from their own poll. Every child is polled with the same Context, so they all register the composing task's waker: whichever child becomes ready wakes the task, the task re-polls the combinator, and the combinator re-polls whichever children have not finished.
One task drives the whole group. No child is spawned, and none needs an executor.
Build them through Futures, which is a namespace rather than a value — a combinator is generic over its children's types as well as their outputs, and a child produced by an async function has a compiler-generated type no caller can name. Constructing through statics whose bounds infer the whole set is what makes them constructible at all.
| Constructor | Completes with |
|---|---|
Futures::join(a, b) | (OA, OB) once both have finished. |
Futures::select(a, b) | Selected<OA, OB> as soon as either finishes. |
Futures::timeout(f, dur) | Result<O, Elapsed>. |
select polls a first on every poll. That bias is deliberate and documented rather than randomized: a select returns on the first ready child, so a consistently-ready a cannot starve b within one select — it only makes the choice predictable when both are ready at once, which is exactly what a caller pairing an operation against a timeout wants.
timeout polls the operation before the deadline, so a future that becomes ready on the very poll where the timer also fires reports its value rather than a timeout. It did complete; calling that a timeout would discard a result the caller can never get back — and for an I/O future, one whose side effect has already happened.
Elapsed carries the deadline it missed, as monotonic nanoseconds on the same scale Instant::as_nanos reports.
Cancellation is a drop
A Select that completes still owns the child that lost, and a Timeout that elapses still owns the operation it was timing. Both are released when the combinator itself is dropped, and that is what cancels them: dropping a parked future runs its Drop, which is where a Sleep disarms its timer and an I/O future releases its reactor registration.
Cancellation therefore needs no separate mechanism and cannot be forgotten — it is the same drop that reclaims the memory.
Arity
These compose two futures. Higher arities nest: Futures::join(a, Futures::join(b, c)). Cryo has no variadic generics, so a fixed-arity pair plus nesting is the whole story; the alternative would be a hand-written Join3, Join4, ... tower that says nothing new.
Polling any of these after it completes panics, matching
Ready. A completed future has already moved its output out, so a second poll has nothing to return — and would re-poll children that already finished, itself a contract violation.
There is no
try_joinyet. Joining two fallible futures and short-circuiting on the first error runs into an inference limitation around generic statics whose return type mentions both the future parameters and types reachable only by destructuring a nested generic bound. Until that is fixed,joinover twoResult-producing futures gives you the concurrency and both results; what it does not give you is cancelling the sibling the moment one side fails.
Sleep
The timer counterpart of an I/O future. Instead of asking the reactor to wake it when a descriptor becomes ready, it asks to be woken at a point on the monotonic clock. The reactor keeps those deadlines in a sorted chain and bounds its readiness wait by the earliest one, so a sleeping task costs no thread and no polling — the wait the reactor was already in simply ends sooner.
Sleep::new(dur: Duration), Sleep::until(deadline: Instant), deadline_nanos(), and is_elapsed().
Deadlines are absolute, not durations: Sleep::new(dur) resolves dur against the clock once, at construction. A Sleep polled late, or polled many times, still completes at the instant it was created to complete at, and re-polling never extends it.
Dropping a Sleep disarms its registration. That is what makes it usable as the losing half of a select — the timer that did not win is dropped and its waker released immediately, rather than lingering until the deadline passes.