@@ -444,7 +444,7 @@ created by `canon_lift` and `Subtask`, which is created by `canon_lower`.
444
444
Additional sync-/async-specialized mutable state is added by the ` AsyncTask `
445
445
and ` AsyncSubtask ` subclasses.
446
446
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:
448
448
``` python
449
449
class AsyncCallState (IntEnum ):
450
450
STARTING = 0
@@ -458,12 +458,18 @@ class EventCode(IntEnum):
458
458
CALL_RETURNED = AsyncCallState.RETURNED
459
459
CALL_DONE = AsyncCallState.DONE
460
460
YIELDED = 4
461
+
462
+ class OnBlockResult (IntEnum ):
463
+ BLOCKED = 0
464
+ COMPLETED = 1
461
465
```
462
466
The ` AsyncCallState ` enum describes the linear sequence of states that an async
463
467
call necessarily transitions through: [ ` STARTING ` ] ( Async.md#starting ) ,
464
468
` STARTED ` , [ ` RETURNING ` ] ( Async.md#returning ) and ` DONE ` . The ` EventCode ` enum
465
469
shares common code values with ` AsyncCallState ` to define the set of integer
466
470
event 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.
467
473
468
474
The ` current_Task ` global holds an ` asyncio.Lock ` that is used to prevent the
469
475
Python runtime from arbitrarily switching between Python coroutines (`async
@@ -481,27 +487,24 @@ current_task = asyncio.Lock()
481
487
482
488
A ` Task ` object is created for each call to ` canon_lift ` and is implicitly
483
489
threaded 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 ) ".
487
491
``` python
488
492
class Task (CallContext ):
489
493
caller: Optional[Task]
490
- on_block: Optional[Callable ]
494
+ on_block: Optional[asyncio.Future[OnBlockResult] ]
491
495
borrow_count: int
492
496
events: asyncio.Queue[AsyncSubtask]
493
497
num_async_subtasks: int
494
498
495
499
def __init__ (self , opts , inst , caller , on_block ):
496
500
super ().__init__ (opts, inst)
497
- assert (on_block is not None )
498
501
self .caller = caller
499
502
self .on_block = on_block
500
503
self .borrow_count = 0
501
504
self .events = asyncio.Queue[AsyncSubtask]()
502
505
self .num_async_subtasks = 0
503
506
```
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.
505
508
Using a conservative syntactic analysis of the component-level definitions of a
506
509
linked component DAG, an optimizing implementation can statically eliminate
507
510
these fields when the particular feature (` borrow ` handles, ` async ` imports) is
@@ -531,16 +534,15 @@ O(n) loop in `trap_if_on_the_stack`:
531
534
instance a static bit position) that is passed by copy from caller to callee.
532
535
533
536
The ` 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.
544
546
``` python
545
547
async def enter (self ):
546
548
assert (current_task.locked())
@@ -569,36 +571,29 @@ before it can re-enable backpressure.
569
571
The rules shown above also ensure that synchronously-lifted exports only
570
572
execute when no other (sync or async) tasks are executing concurrently.
571
573
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.
586
578
``` python
587
579
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 )
592
582
else :
593
583
current_task.release()
594
- r = await future
584
+ v = await future
595
585
await current_task.acquire()
596
- return r
586
+ return v
597
587
```
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 ` .
602
597
603
598
While a task is running, it may call ` wait ` (via ` canon task.wait ` or, when a
604
599
` callback ` is present, by returning to the event loop) to block until there is
@@ -672,13 +667,7 @@ after this export call finishes):
672
667
```
673
668
674
669
Lastly, 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.
682
671
``` python
683
672
def exit (self ):
684
673
assert (current_task.locked())
@@ -690,8 +679,6 @@ resource allocation.
690
679
if self .opts.sync:
691
680
self .inst.may_not_enter_bc_sync_export = False
692
681
self .maybe_start_pending_task()
693
- if not self .on_block:
694
- current_task.release()
695
682
```
696
683
697
684
While ` canon_lift ` creates ` Task ` s, ` canon_lower ` creates ` Subtask ` objects:
@@ -1906,8 +1893,8 @@ When instantiating component instance `$inst`:
1906
1893
The resulting function ` $f ` takes 4 runtime arguments:
1907
1894
* ` caller ` : the caller's ` Task ` or, if this lifted function is being called by
1908
1895
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
1911
1898
* ` on_start ` : a nullary function that must be called to return the caller's
1912
1899
arguments as a list of component-level values
1913
1900
* ` on_return ` : a unary function that must be called after ` on_start ` ,
@@ -2021,31 +2008,26 @@ The resulting function `$f` takes 2 runtime arguments:
2021
2008
Given this, ` canon_lower ` is defined:
2022
2009
``` python
2023
2010
async def canon_lower (opts , callee , ft , task , flat_args ):
2011
+ assert (current_task.locked())
2024
2012
trap_if(task.inst.may_not_leave)
2025
2013
2026
2014
flat_args = CoreValueIter(flat_args)
2027
2015
flat_results = None
2028
2016
if opts.sync:
2029
2017
subtask = Subtask(opts, task.inst)
2030
2018
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
2035
2019
def on_start ():
2036
2020
return lift_flat_values(subtask, MAX_FLAT_PARAMS , flat_args, ft.param_types())
2037
2021
def on_return (results ):
2038
2022
nonlocal flat_results
2039
2023
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)
2041
2025
task.inst.may_not_enter_bc_sync_import = False
2042
2026
subtask.finish()
2043
2027
else :
2044
2028
subtask = AsyncSubtask(opts, task.inst)
2045
- eager_result = asyncio.Future()
2029
+ on_block = asyncio.Future()
2046
2030
async def do_call ():
2047
- def on_block ():
2048
- eager_result.set_result(' block' )
2049
2031
def on_start ():
2050
2032
subtask.start()
2051
2033
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):
2054
2036
lower_flat_values(subtask, 0 , results, ft.result_types(), flat_args)
2055
2037
await callee(task, on_block, on_start, on_return)
2056
2038
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 )
2059
2043
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 :
2064
2046
i = task.add_async_subtask(subtask)
2065
2047
flat_results = [pack_async_result(i, subtask.state)]
2048
+ case OnBlockResult.COMPLETED :
2049
+ flat_results = [0 ]
2066
2050
2051
+ assert (current_task.locked())
2067
2052
return flat_results
2068
2053
```
2069
2054
In 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.
2084
2066
2085
2067
Whether 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.
2092
2072
2093
2073
If an ` async ` -lowered call blocks, the ` AsyncSubtask ` is added to the component
2094
2074
instance's ` async_subtasks ` table, and the index and state are returned to the
@@ -2473,9 +2453,7 @@ def canon_thread_hw_concurrency():
2473
2453
[ Exceptions ] : https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md
2474
2454
[ WASI ] : https://github.com/webassembly/wasi
2475
2455
[ 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
2479
2457
2480
2458
[ Alignment ] : https://en.wikipedia.org/wiki/Data_structure_alignment
2481
2459
[ UTF-8 ] : https://en.wikipedia.org/wiki/UTF-8
0 commit comments