@@ -444,7 +444,7 @@ created by `canon_lift` and `Subtask`, which is created by `canon_lower`.
444444Additional sync-/async-specialized mutable state is added by the ` AsyncTask `
445445and ` AsyncSubtask ` subclasses.
446446
447- The ` Task ` class and its subclasses depend on the following two enums:
447+ The ` Task ` class and its subclasses depend on the following three enums:
448448``` python
449449class AsyncCallState (IntEnum ):
450450 STARTING = 0
@@ -458,12 +458,18 @@ class EventCode(IntEnum):
458458 CALL_RETURNED = AsyncCallState.RETURNED
459459 CALL_DONE = AsyncCallState.DONE
460460 YIELDED = 4
461+
462+ class OnBlockResult (IntEnum ):
463+ BLOCKED = 0
464+ COMPLETED = 1
461465```
462466The ` AsyncCallState ` enum describes the linear sequence of states that an async
463467call necessarily transitions through: [ ` STARTING ` ] ( Async.md#starting ) ,
464468` STARTED ` , [ ` RETURNING ` ] ( Async.md#returning ) and ` DONE ` . The ` EventCode ` enum
465469shares common code values with ` AsyncCallState ` to define the set of integer
466470event codes that are delivered to [ waiting] ( Async.md#waiting ) or polling tasks.
471+ The ` OnBlockResult ` enum conveys the two possible results of the ` on_block `
472+ future used to tell callers whether or not the callee blocked.
467473
468474The ` current_Task ` global holds an ` asyncio.Lock ` that is used to prevent the
469475Python runtime from arbitrarily switching between Python coroutines (`async
@@ -481,27 +487,24 @@ current_task = asyncio.Lock()
481487
482488A ` Task ` object is created for each call to ` canon_lift ` and is implicitly
483489threaded through all core function calls. This implicit ` Task ` parameter
484- specifies a concept of [ the current task] ( Async.md#current-task ) and inherently
485- scopes execution of all core wasm (including ` canon ` -defined core functions) to
486- a ` Task ` .
490+ represents "[ the current task] ( Async.md#current-task ) ".
487491``` python
488492class Task (CallContext ):
489493 caller: Optional[Task]
490- on_block: Optional[Callable ]
494+ on_block: Optional[asyncio.Future[OnBlockResult] ]
491495 borrow_count: int
492496 events: asyncio.Queue[AsyncSubtask]
493497 num_async_subtasks: int
494498
495499 def __init__ (self , opts , inst , caller , on_block ):
496500 super ().__init__ (opts, inst)
497- assert (on_block is not None )
498501 self .caller = caller
499502 self .on_block = on_block
500503 self .borrow_count = 0
501504 self .events = asyncio.Queue[AsyncSubtask]()
502505 self .num_async_subtasks = 0
503506```
504- The fields of ` Task ` are introduced in groups of related ` Task ` - methods next.
507+ The fields of ` Task ` are introduced in groups of related ` Task ` methods next.
505508Using a conservative syntactic analysis of the component-level definitions of a
506509linked component DAG, an optimizing implementation can statically eliminate
507510these fields when the particular feature (` borrow ` handles, ` async ` imports) is
@@ -531,16 +534,15 @@ O(n) loop in `trap_if_on_the_stack`:
531534 instance a static bit position) that is passed by copy from caller to callee.
532535
533536The ` enter ` method is called immediately after constructing a ` Task ` and, along
534- with the ` may_enter ` and ` may_start_pending_task ` helper functions, implements
535- backpressure. If a ` Task ` tries to ` enter ` when ` may_enter ` is false, the
536- ` Task ` suspends itself (via ` suspend ` , shown next) and goes into a
537- ` pending_tasks ` queue, waiting to be unblocked when ` may_enter ` is true by
538- another task calling ` maybe_start_pending_task ` . One key property of this
539- backpressure scheme is that ` pending_tasks ` are only dequeued one at a time,
540- ensuring that if an overloaded component instance enables backpressure (via
541- ` task.backpressure ` ) and then disables it, there will not be an unstoppable
542- thundering herd of pending tasks started all at once that OOM the component
543- before it can re-enable backpressure.
537+ with ` may_enter ` and ` may_start_pending_task ` , implements backpressure. If a
538+ ` Task ` tries to ` enter ` when ` may_enter ` is false, the ` Task ` suspends itself
539+ (via ` suspend ` , shown next) and goes into a ` pending_tasks ` queue, waiting to
540+ be unblocked by another task calling ` maybe_start_pending_task ` . One key
541+ property of this backpressure scheme is that ` pending_tasks ` are only dequeued
542+ one at a time, ensuring that if an overloaded component instance enables
543+ backpressure (via ` task.backpressure ` ) and then disables it, there will not be
544+ an unstoppable thundering herd of pending tasks started all at once that OOM
545+ the component before it can re-enable backpressure.
544546``` python
545547 async def enter (self ):
546548 assert (current_task.locked())
@@ -569,36 +571,29 @@ before it can re-enable backpressure.
569571The rules shown above also ensure that synchronously-lifted exports only
570572execute when no other (sync or async) tasks are executing concurrently.
571573
572- The ` suspend ` method, used by ` enter ` , ` wait ` and ` yield_ ` , takes an
573- ` asyncio.Future ` to ` await ` and allows other tasks to make progress in the
574- meantime. When suspending, there are two cases to consider:
575- * This is the first time the current ` Task ` has blocked and thus there may be
576- an ` async ` -lowered caller waiting to find out that the callee blocked (which
577- is signalled by calling the ` on_block ` handler that the caller passed to
578- ` canon_lift ` ).
579- * This task has already blocked in the past (signalled by ` on_block ` being
580- ` None ` ) and thus there is no ` async ` -lowered caller to switch to and so we
581- let Python's ` asyncio ` scheduler non-deterministically pick some other task
582- that is ready to go by releasing the ` current_task ` lock.
583-
584- In either case, once the given future is resolved, this ` Task ` has to
585- re-` acquire ` the ` current_stack ` lock to run again.
574+ The ` suspend ` method, called by ` enter ` , ` wait ` and ` yield_ ` , takes a future to
575+ ` await ` and allows other tasks to make progress in the meantime. Once this
576+ future is resolved, the current task must reacquire the ` current_task ` lock to
577+ wait for any other task that is currently executing to suspend or exit.
586578``` python
587579 async def suspend (self , future ):
588- assert (current_task.locked())
589- if self .on_block:
590- self .on_block()
591- self .on_block = None
580+ if self .on_block and not self .on_block.done():
581+ self .on_block.set_result(OnBlockResult.BLOCKED )
592582 else :
593583 current_task.release()
594- r = await future
584+ v = await future
595585 await current_task.acquire()
596- return r
586+ return v
597587```
598- As a side note: the ` suspend ` method is so named because it could be
599- reimplemented using the [ ` suspend ` ] instruction of the [ typed continuations]
600- proposal, removing the need for ` on_block ` and the subtle calling contract
601- between ` suspend ` and ` canon_lift ` .
588+ When there is an ` async ` -lowered caller waiting on the stack, the ` on_block `
589+ field will point to an unresolved future. In this case, ` suspend ` sets the
590+ result of ` on_block ` and leaves ` current_task ` locked so that control flow
591+ transfers deterministically to ` async ` -lowered caller (in ` canon_lower ` ,
592+ defined below). The ` suspend ` method is so named because this delicate use of
593+ Python's async functionality is essentially emulating the ` suspend ` /` resume `
594+ instructions of the [ stack-switching] proposal. Thus, once stack-switching is
595+ implemented, a valid implementation technique would be to compile Canonical ABI
596+ adapters to Core WebAssembly using ` suspend ` and ` resume ` .
602597
603598While a task is running, it may call ` wait ` (via ` canon task.wait ` or, when a
604599` callback ` is present, by returning to the event loop) to block until there is
@@ -672,13 +667,7 @@ after this export call finishes):
672667```
673668
674669Lastly, when a task exits, the runtime enforces the guard conditions mentioned
675- above and allows other tasks to start or make progress. If the exiting ` Task `
676- has not yet blocked, there is an active ` async ` -lowered caller on the stack, so
677- we don't release the ` current_task ` lock and instead just let the ` Task ` 's
678- Python coroutine return directly to the ` await ` ing caller without any possible
679- task switch. The net effect is that when a cross-component async starts and
680- finishes without blocking, there doesn't need to be stack switching or async
681- resource allocation.
670+ above and allows pending tasks to start.
682671``` python
683672 def exit (self ):
684673 assert (current_task.locked())
@@ -690,8 +679,6 @@ resource allocation.
690679 if self .opts.sync:
691680 self .inst.may_not_enter_bc_sync_export = False
692681 self .maybe_start_pending_task()
693- if not self .on_block:
694- current_task.release()
695682```
696683
697684While ` canon_lift ` creates ` Task ` s, ` canon_lower ` creates ` Subtask ` objects:
@@ -1906,8 +1893,8 @@ When instantiating component instance `$inst`:
19061893The resulting function ` $f ` takes 4 runtime arguments:
19071894* ` caller ` : the caller's ` Task ` or, if this lifted function is being called by
19081895 the host, ` None `
1909- * ` on_block ` : a nullary function that must be called at most once by the callee
1910- before blocking the first time
1896+ * ` on_block ` : an optional ` asyncio.Future ` that must be resolved with
1897+ ` OnBlockResult.BLOCKED ` if the callee blocks on I/O
19111898* ` on_start ` : a nullary function that must be called to return the caller's
19121899 arguments as a list of component-level values
19131900* ` on_return ` : a unary function that must be called after ` on_start ` ,
@@ -2021,31 +2008,26 @@ The resulting function `$f` takes 2 runtime arguments:
20212008Given this, ` canon_lower ` is defined:
20222009``` python
20232010async def canon_lower (opts , callee , ft , task , flat_args ):
2011+ assert (current_task.locked())
20242012 trap_if(task.inst.may_not_leave)
20252013
20262014 flat_args = CoreValueIter(flat_args)
20272015 flat_results = None
20282016 if opts.sync:
20292017 subtask = Subtask(opts, task.inst)
20302018 task.inst.may_not_enter_bc_sync_import = True
2031- def on_block ():
2032- if task.on_block:
2033- task.on_block()
2034- task.on_block = None
20352019 def on_start ():
20362020 return lift_flat_values(subtask, MAX_FLAT_PARAMS , flat_args, ft.param_types())
20372021 def on_return (results ):
20382022 nonlocal flat_results
20392023 flat_results = lower_flat_values(subtask, MAX_FLAT_RESULTS , results, ft.result_types(), flat_args)
2040- await callee(task, on_block, on_start, on_return)
2024+ await callee(task, task. on_block, on_start, on_return)
20412025 task.inst.may_not_enter_bc_sync_import = False
20422026 subtask.finish()
20432027 else :
20442028 subtask = AsyncSubtask(opts, task.inst)
2045- eager_result = asyncio.Future()
2029+ on_block = asyncio.Future()
20462030 async def do_call ():
2047- def on_block ():
2048- eager_result.set_result(' block' )
20492031 def on_start ():
20502032 subtask.start()
20512033 return lift_flat_values(subtask, 1 , flat_args, ft.param_types())
@@ -2054,41 +2036,39 @@ async def canon_lower(opts, callee, ft, task, flat_args):
20542036 lower_flat_values(subtask, 0 , results, ft.result_types(), flat_args)
20552037 await callee(task, on_block, on_start, on_return)
20562038 subtask.finish()
2057- if not eager_result.done():
2058- eager_result.set_result(' complete' )
2039+ if on_block.done():
2040+ current_task.release()
2041+ else :
2042+ on_block.set_result(OnBlockResult.COMPLETED )
20592043 asyncio.create_task(do_call())
2060- match await eager_result:
2061- case ' complete' :
2062- flat_results = [0 ]
2063- case ' block' :
2044+ match await on_block:
2045+ case OnBlockResult.BLOCKED :
20642046 i = task.add_async_subtask(subtask)
20652047 flat_results = [pack_async_result(i, subtask.state)]
2048+ case OnBlockResult.COMPLETED :
2049+ flat_results = [0 ]
20662050
2051+ assert (current_task.locked())
20672052 return flat_results
20682053```
20692054In the synchronous case, the import call is bracketed by setting
2070- ` calling_sync_import ` to prevent reentrance into the current component instance
2071- if the ` callee ` blocks and the caller gets control flow (via ` on_block ` ). Like
2072- ` Task.suspend ` above, ` canon_lift ` clears the ` on_block ` handler after calling
2073- to signal that the current ` Task ` has already released any waiting
2074- ` async ` -lowered callers.
2075-
2076- In the asynchronous case, we finally see the whole point of ` on_block ` which is
2077- to allow us to wait for one of two outcomes: the callee blocks or the callee
2078- finishes without blocking. Whichever happens first resolves the ` eager_result `
2079- future. After calling ` asyncio.create_task ` , ` canon_lift ` immediately ` await ` s
2080- ` eager_result ` so that there is no allowed interleaving between the caller and
2081- callee's Python coroutines. This overall behavior resembles the [ ` resume ` ]
2082- instruction of the [ typed continuations] proposal (handling a ` block ` effect)
2083- which could be used to more-directly implement the Python control flow here.
2055+ ` may_not_enter_bc_sync_import ` to prevent reentrance into the current component
2056+ instance if the ` callee ` blocks and the caller gets control flow (via
2057+ ` on_block ` ).
2058+
2059+ In the asynchronous case, the ` on_block ` future allows the caller to ` await ` to
2060+ see if ` callee ` blocks on I/O before returning. Because ` Task.suspend ` (defined
2061+ above) does not release the ` current_task ` lock when it resolves ` on_block ` to
2062+ ` BLOCKED ` , control flow deterministically returns to the caller (without
2063+ executing other tasks) when ` callee ` blocks. This is analogous to how the
2064+ ` resume ` instruction of the [ stack-switching] proposal would work if given
2065+ ` (cont.new $callee) ` and handling an ` on_block ` event.
20842066
20852067Whether or not the ` callee ` blocks, the ` on_start ` and ` on_return ` handlers
2086- must be called before the ` callee ` completes (either by ` canon_lift ` in the
2087- synchronous case or the ` task.start ` /` task.return ` built-ins in the
2088- asynchronous case). Note that, when ` async ` -lowering, lifting and lowering
2089- can happen after ` canon_lower ` returns and thus the caller must ` task.wait `
2090- for ` EventCode ` s to know when the supplied linear memory pointers can be
2091- reused.
2068+ will be called before the ` callee ` completes. Note that, in an ` async ` -lowered
2069+ call, ` on_start ` and ` on_return ` can happen after ` canon_lower ` returns and
2070+ thus the caller must ` task.wait ` for progress events to know when the supplied
2071+ linear memory pointers can be reclaimed by the caller.
20922072
20932073If an ` async ` -lowered call blocks, the ` AsyncSubtask ` is added to the component
20942074instance's ` async_subtasks ` table, and the index and state are returned to the
@@ -2473,9 +2453,7 @@ def canon_thread_hw_concurrency():
24732453[ Exceptions ] : https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md
24742454[ WASI ] : https://github.com/webassembly/wasi
24752455[ Deterministic Profile ] : https://github.com/WebAssembly/profiles/blob/main/proposals/profiles/Overview.md
2476- [ Typed Continuations ] : https://github.com/WebAssembly/stack-switching/blob/main/proposals/continuations/Explainer.md
2477- [ `suspend` ] : https://github.com/WebAssembly/stack-switching/blob/main/proposals/continuations/Explainer.md#suspending-continuations
2478- [ `resume` ] : https://github.com/WebAssembly/stack-switching/blob/main/proposals/continuations/Explainer.md#invoking-continuations
2456+ [ stack-switching ] : https://github.com/WebAssembly/stack-switching
24792457
24802458[ Alignment ] : https://en.wikipedia.org/wiki/Data_structure_alignment
24812459[ UTF-8 ] : https://en.wikipedia.org/wiki/UTF-8
0 commit comments