Module async/waker
Waker and park — the "resume THIS task" primitive.
Yo's async runtime can suspend a task on I/O, but until this module there
was no way for one task to be woken by ANOTHER task's progress. So every
std primitive that waits on a peer polled a clock — yield, a contended
async.Mutex.lock, a blocked async.Channel send or recv — which puts a
millisecond floor under every hand-off: a producer/consumer pair caps
at ~1000 hand-offs per second no matter how fast the work is.
(plans/archive/WAKER_BASED_SCHEDULING.md.)
{ Park } :: import("std/async/waker");
// The waiter: create the park, hand its waker to whoever will signal,
// then suspend. Nothing may await between those three steps.
p := Park.new();
waiters.push(p.waker());
io.await(p.wait(io), io);
// The signaller, from any other task:
match(waiters.pop(), .Some(w) => w.wake(), .None => ());
Why the order matters, and why it is not a race here
The classic mistake is to suspend and THEN publish the waker: the wake can
arrive in between and be lost. Publishing first is safe, and in a
single-threaded loop it is not even close — no other task can run between
waker() and wait(), because those are ordinary synchronous calls.
It stays safe even when a wake DOES arrive first, which is the property
that matters if the loop ever becomes multi-threaded: wake completes the
park future, and an await point reads the future's state BEFORE it
registers a continuation (src/codegen/async/state_machine.yo), so an
already-woken park resumes inline instead of suspending. park below wraps
the whole sequence for callers who only need one waker and would rather not
be able to get the order wrong.
Waking is idempotent
A second wake(), or a wake() of a park whose task is already gone, does
nothing. That is what lets a waiter list signal everyone without tracking
who already ran.
Stability
unstable — the mechanism std/async/channel and std/async/mutex wait
on, and the one std/async's combinators will move to when they become
futures (phase A1 of plans/ASYNC_IO_API_AUDIT.md).
Types
A token that makes one suspended task runnable again.
Safe to fire more than once, and safe to fire from a task that is not the
sleeper. It holds a REFERENCE to the park it resumes, so the token cannot
outlive its target — a non-owning pointer here would be the use-after-free
Watcher's event-loop callback once had
(issues/fixed/a-dropped-watcher-leaves-the-event-loop-calling-freed-memory.md).
A Waker is a move-only value (plans/VALUES_BY_DEFAULT.md §3.13 A5): it
has one owner, and dropping it releases its token. A waiter list moves
the waker in (waiters.push(p.waker())) and moves it back out to fire it
(waiters.remove(usize(0))); a closure that fires it from another thread
takes it by move. It is Send: a waker exists to be handed to whoever
will signal, and since step 5 of the waker campaign that includes a worker
thread (spawn_blocking). A cross-thread wake() does not touch the park
future — it hands the token to the future's own loop and nudges it — and
the token's own count is atomic, so the worker may also drop it.
clone makes another handle on the same token, so the owner can keep one
while a worker thread wakes and drops the other.
Fields
| Name | Type | Description |
|---|---|---|
_p | *(u8) | The waker token, holding one reference to the park it resumes. |
Trait Implementations
impl(Waker, Send())
impl(Waker, Sync())
impl(Waker, ...)
wake : (Waker) fn(self : &Waker) -> unitMake the parked task runnable. A no-op if it was already woken, or if its task ended without ever suspending.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Waker |
Returns: unit
is_woken : (Waker) fn(self : &Waker) -> boolWhether the park has been woken. Non-blocking. false for a park whose
task was cancelled (it will never run): a waiter list that just called
wake asks this to learn whether the wake reached a live task, and
passes it on to the next waiter if not. From another thread, false
may still turn true once the owner loop drains the wake.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Waker |
Returns: bool
impl(Waker, Clone(...))
impl(Waker, Drop(...))
drop : (Waker) fn(self : &mut Waker) -> unitOne ArcWeak fewer; the cell is freed when its payload is gone and
this was the last handle. A zeroed handle names no cell.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Waker |
Returns: unit
A suspension point that a Waker resumes.
Create it, hand out as many wakers as there are signallers, then wait.
This is the primitive a waiter LIST wants — a Mutex or a Channel stores
the waker in its queue and suspends — which is why it is exposed beside the
closure-shaped park below.
A Park is a move-only value (its future is, decision 13): wait
consumes it, so one park is waited on once.
Fields
| Name | Type | Description |
|---|---|---|
_future | Impl(Future(i32)) |
impl(Park, ...)
new : (Park) fn() -> ParkA fresh, unwoken park.
Returns: Park
waker : (Park) fn(self : &Park) -> WakerFunctions
Suspend the current task until the waker handed to register is woken.
register runs BEFORE the suspension and receives the waker by move, so
it can hand it to whoever will fire it without any chance of publishing it
too late. A waker register drops instead releases its token, and a park
nothing can wake is reported by the loop rather than hung. This is the shape Rust's Future::poll(cx) and every correct condvar
API use, and it is the right default for a caller who needs exactly one
waker; reach for Park directly when a waiter list wants to hold the token
itself.
Parameters
| Name | Type | Notes |
|---|---|---|
register | &Impl(Fn(w : Waker) -> unit) | |
io | Io |
Returns: Impl(Future(unit, Io))
Give the event loop ONE turn, including an I/O poll, and come back — a fairness yield with no timer under it.
std/async's yield is this, as of v0.2.32 — the two are the same
mechanism and either name is fine. It could not be until then for a
bootstrap reason, not a design one: yield is on the compiler's own import
path (through std/fs/watch), and the SEED compiler that builds this tree
emits the async runtime IT was built with — so pointing yield here failed
to LINK the compiler until a published seed carried
__yo_async_yield_start. v0.2.31 is the first that does.
The mechanism: the future is created PENDING and completed at the top of
the next ready-task drain, after that drain has measured its budget, so the
resumed continuation lands beyond the budget and runs in the following step
— with exactly one __yo_io_poll() in between. An already-complete future
would not do: the await point takes an inline fast path for one of those
and the task never leaves the C stack, which is the spin that once made a
poll-until-finished loop starve I/O
(issues/fixed/build-smoke-hangs-registry-perturbation.md).
Parameters
| Name | Type | Notes |
|---|---|---|
io | Io |
Returns: Impl(Future(unit, Io))