@@ -444,6 +444,27 @@ created by `canon_lift` and `Subtask`, which is created by `canon_lower`.
444444Additional sync-/async-specialized mutable state is added by the ` SyncTask ` ,
445445` AsyncTask ` and ` AsyncSubtask ` subclasses.
446446
447+ The ` Task ` class and its subclasses depend on the following two enums:
448+ ``` python
449+ class AsyncCallState (IntEnum ):
450+ STARTING = 0
451+ STARTED = 1
452+ RETURNED = 2
453+ DONE = 3
454+
455+ class EventCode (IntEnum ):
456+ CALL_STARTING = AsyncCallState.STARTING
457+ CALL_STARTED = AsyncCallState.STARTED
458+ CALL_RETURNED = AsyncCallState.RETURNED
459+ CALL_DONE = AsyncCallState.DONE
460+ YIELDED = 4
461+ ```
462+ The ` AsyncCallState ` enum describes the linear sequence of states that an async
463+ call necessarily transitions through: [ ` STARTING ` ] ( Async.md#starting ) ,
464+ ` STARTED ` , [ ` RETURNING ` ] ( Async.md#returning ) and ` DONE ` . The ` EventCode ` enum
465+ shares common code values with ` AsyncCallState ` to define the set of integer
466+ event codes that are delivered to [ waiting] ( Async.md#waiting ) or polling tasks.
467+
447468A ` Task ` object is created for each call to ` canon_lift ` and is implicitly
448469threaded through all core function calls. This implicit ` Task ` parameter
449470specifies a concept of [ the current task] ( Async.md#current-task ) and inherently
@@ -520,8 +541,7 @@ All `Task`s (whether lifted `async` or not) are allowed to call `async`-lowered
520541imports. Calling an ` async ` -lowered import creates an ` AsyncSubtask ` (defined
521542below) which is stored in the current component instance's ` async_subtasks `
522543table and tracked by the current task's ` num_async_subtasks ` counter, which is
523- guarded to be ` 0 ` in ` Task.exit ` (below) to ensure the
524- tree-structured-concurrency [ component invariant] .
544+ guarded to be ` 0 ` in ` Task.exit ` (below) to ensure [ structured concurrency] .
525545``` python
526546 def add_async_subtask (self , subtask ):
527547 assert (subtask.supertask is None and subtask.index is None )
@@ -549,7 +569,7 @@ tree-structured-concurrency [component invariant].
549569 if subtask.state == AsyncCallState.DONE :
550570 self .inst.async_subtasks.remove(subtask.index)
551571 self .num_async_subtasks -= 1
552- return (subtask.state, subtask.index)
572+ return (EventCode( subtask.state) , subtask.index)
553573```
554574While a task is running, it may call ` wait ` (via ` canon task.wait ` or, when a
555575` callback ` is present, by returning to the event loop) to block until there is
@@ -573,6 +593,16 @@ another task:
573593 return self .process_event(self .events.get_nowait())
574594```
575595
596+ A task may also cooperatively yield the current thread, explicitly allowing
597+ the runtime to switch to another ready task, but without blocking on I/O (as
598+ emulated in the Python code here by awaiting a ` sleep(0) ` ).
599+ ``` python
600+ async def yield_ (self ):
601+ self .inst.thread.release()
602+ await asyncio.sleep(0 )
603+ await self .inst.thread.acquire()
604+ ```
605+
576606Lastly, when a task exists, the runtime enforces the guard conditions mentioned
577607above and releases the ` thread ` lock, allowing other tasks to start or make
578608progress.
@@ -641,17 +671,6 @@ implementation should be able to avoid separately allocating
641671` pending_sync_tasks ` by instead embedding a "next pending" linked list in the
642672` Subtask ` table element of the caller.
643673
644- The ` AsyncTask ` class dynamically checks that the task calls the
645- ` canon_task_start ` and ` canon_task_return ` (defined below) in the right order
646- before finishing the task. "The right order" is defined in terms of a simple
647- linear state machine that progresses through the following 4 states:
648- ``` python
649- class AsyncCallState (IntEnum ):
650- STARTING = 0
651- STARTED = 1
652- RETURNED = 2
653- DONE = 3
654- ```
655674The first 3 fields of ` AsyncTask ` are simply immutable copies of
656675arguments/immediates passed to ` canon_lift ` that are used later on. The last 2
657676fields are used to check the above-mentioned state machine transitions and also
@@ -1952,10 +1971,16 @@ async def canon_lift(opts, inst, callee, ft, caller, start_thunk, return_thunk):
19521971 if not opts.callback:
19531972 [] = await call_and_trap_on_throw(callee, task, [])
19541973 else :
1955- [ctx] = await call_and_trap_on_throw(callee, task, [])
1956- while ctx != 0 :
1957- event, payload = await task.wait()
1958- [ctx] = await call_and_trap_on_throw(opts.callback, task, [ctx, event, payload])
1974+ [packed_ctx] = await call_and_trap_on_throw(callee, task, [])
1975+ while packed_ctx != 0 :
1976+ is_yield = bool (packed_ctx & 1 )
1977+ ctx = packed_ctx & ~ 1
1978+ if is_yield:
1979+ await task.yield_()
1980+ event, payload = (EventCode.YIELDED , 0 )
1981+ else :
1982+ event, payload = await task.wait()
1983+ [packed_ctx] = await call_and_trap_on_throw(opts.callback, task, [ctx, event, payload])
19591984
19601985 assert (opts.post_return is None )
19611986 task.exit()
@@ -1983,11 +2008,13 @@ allow the callee to reclaim any memory. An async call doesn't need a
19832008
19842009Within the async case, there are two sub-cases depending on whether the
19852010` callback ` ` canonopt ` was set. When ` callback ` is present, waiting happens in
1986- an "event loop" inside ` canon_lift ` . Otherwise, waiting must happen by calling
1987- ` task.wait ` (defined below), which potentially requires the runtime
1988- implementation to use a fiber (aka. stackful coroutine) to switch to another
1989- task. Thus, ` callback ` is an optimization for avoiding fiber creation for async
1990- languages that don't need it (e.g., JS, Python, C# and Rust).
2011+ an "event loop" inside ` canon_lift ` which also allows yielding (i.e., allowing
2012+ other tasks to run without blocking) by setting the LSB of the returned ` i32 ` .
2013+ Otherwise, waiting must happen by calling ` task.wait ` (defined below), which
2014+ potentially requires the runtime implementation to use a fiber (aka. stackful
2015+ coroutine) to switch to another task. Thus, ` callback ` is an optimization for
2016+ avoiding fiber creation for async languages that don't need it (e.g., JS,
2017+ Python, C# and Rust).
19912018
19922019Uncaught Core WebAssembly [ exceptions] result in a trap at component
19932020boundaries. Thus, if a component wishes to signal an error, it must use some
@@ -2332,9 +2359,8 @@ Python `asyncio.sleep(0)` in the middle to make it clear that other
23322359coroutines are allowed to acquire the ` lock ` and execute.
23332360``` python
23342361async def canon_task_yield (task ):
2335- task.inst.thread.release()
2336- await asyncio.sleep(0 )
2337- await task.inst.thread.acquire()
2362+ trap_if(task.opts.callback is not None )
2363+ await task.yield_()
23382364 return []
23392365```
23402366
@@ -2415,6 +2441,7 @@ def canon_thread_hw_concurrency():
24152441[ JavaScript Embedding ] : Explainer.md#JavaScript-embedding
24162442[ Adapter Functions ] : FutureFeatures.md#custom-abis-via-adapter-functions
24172443[ Shared-Everything Dynamic Linking ] : examples/SharedEverythingDynamicLinking.md
2444+ [ Structured Concurrency ] : Async.md#structured-concurrency
24182445
24192446[ Administrative Instructions ] : https://webassembly.github.io/spec/core/exec/runtime.html#syntax-instr-admin
24202447[ Implementation Limits ] : https://webassembly.github.io/spec/core/appendix/implementation.html
0 commit comments