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/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
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
| Name | Type | Description |
|---|---|---|
_p | *(u8) | The waker token, holding one reference to the park it resumes. |
Trait Implementations
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 already been woken. Non-blocking, and only useful
for diagnostics: by the time a caller acts on false it may be true.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Waker |
Returns: bool
impl(Waker, Dispose(...))
dispose : (Waker) fn(self : Waker) -> unitRelease 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
| 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.
Fields
| Name | Type | Description |
|---|---|---|
_future | Impl : (Future[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, 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
| 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/build-smoke-hangs-registry-perturbation.md).
Parameters
| Name | Type | Notes |
|---|---|---|
io | Io |
Returns: Impl(Future(unit, Io))