Module async/waker

async/waker
Stability: unstable — new in this release, and it is the mechanism the four `std/async` modules' stability notes say will change under them. — stable modules only change additively; this one may still change.

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/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 — new in this release, and it is the mechanism the four std/async modules' stability notes say will change under them.

Types

Waker atomic object
Waker

A token that makes one suspended task runnable again.

Cheap to copy, 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). ATOMIC, and therefore 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 this token to the future's own loop and nudges it — so the only refcount that crosses a thread boundary is this token's own, which is what atomic makes safe.

Fields

NameTypeDescription
_p*(u8)

The waker token, holding one reference to the park it resumes.

Trait Implementations

impl(Waker, ...)
wake : (Waker) fn(self : Waker) -> unit

Make the parked task runnable. A no-op if it was already woken, or if its task ended without ever suspending.

Parameters

NameTypeNotes
selfWaker

Returns: unit

is_woken : (Waker) fn(self : Waker) -> bool

Whether the park has already been woken. Non-blocking, and only useful for diagnostics: by the time a caller acts on false it may be true.

Parameters

NameTypeNotes
selfWaker

Returns: bool

impl(Waker, Dispose(...))
dispose : (Waker) fn(self : Waker) -> unit

Release the resources self owns — a file descriptor, a socket, a lock, a buffer the allocator handed out. Called automatically when the last reference to the value goes away, so an implementor never calls it directly and must tolerate being the only one who ever does.

It must be safe to run exactly once: the runtime calls it at refcount zero, and a type that also exposes an explicit close/release is responsible for making the second call a no-op.

Parameters

NameTypeNotes
selfWaker

Returns: unit

Park object
Park

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.

Fields

NameTypeDescription
_futureImpl : (Future[Future](i32))
impl(Park, ...)
new : (Park) fn() -> Park

A fresh, unwoken park.

Returns: Park

waker : (Park) fn(self : Park) -> Waker

A token that will resume whoever is waiting on this park. Call it before wait, and as many times as there are signallers.

Parameters

NameTypeNotes
selfPark

Returns: Waker

wait : (Park) fn(self : Park, io : Io) -> Impl : (Future[Future](unit) Io : Io)

Suspend the current task until one of this park's wakers fires. Resolves immediately if a wake already arrived.

Parameters

NameTypeNotes
selfPark
ioIo

Returns: Impl : (Future[Future](unit) Io : Io)

Functions

park function
fn(register : Impl(Fn(w : Waker) -> unit), io : Io) -> Impl(Future(unit, Io))

Suspend the current task until the waker handed to register is woken.

register runs BEFORE the suspension and receives the waker, so it can hand it to whoever will fire it without any chance of publishing it too late. 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

NameTypeNotes
registerImpl(Fn(w : Waker) -> unit)
ioIo

Returns: Impl(Future(unit, Io))

yield_now function
fn(io : Io) -> 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/build-smoke-hangs-registry-perturbation.md).

Parameters

NameTypeNotes
ioIo

Returns: Impl(Future(unit, Io))