@@ -444,6 +444,27 @@ created by `canon_lift` and `Subtask`, which is created by `canon_lower`.
444
444
Additional sync-/async-specialized mutable state is added by the ` SyncTask ` ,
445
445
` AsyncTask ` and ` AsyncSubtask ` subclasses.
446
446
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
+
447
468
A ` Task ` object is created for each call to ` canon_lift ` and is implicitly
448
469
threaded through all core function calls. This implicit ` Task ` parameter
449
470
specifies 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
520
541
imports. Calling an ` async ` -lowered import creates an ` AsyncSubtask ` (defined
521
542
below) which is stored in the current component instance's ` async_subtasks `
522
543
table 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] .
525
545
``` python
526
546
def add_async_subtask (self , subtask ):
527
547
assert (subtask.supertask is None and subtask.index is None )
@@ -549,7 +569,7 @@ tree-structured-concurrency [component invariant].
549
569
if subtask.state == AsyncCallState.DONE :
550
570
self .inst.async_subtasks.remove(subtask.index)
551
571
self .num_async_subtasks -= 1
552
- return (subtask.state, subtask.index)
572
+ return (EventCode( subtask.state) , subtask.index)
553
573
```
554
574
While a task is running, it may call ` wait ` (via ` canon task.wait ` or, when a
555
575
` callback ` is present, by returning to the event loop) to block until there is
@@ -573,6 +593,16 @@ another task:
573
593
return self .process_event(self .events.get_nowait())
574
594
```
575
595
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
+
576
606
Lastly, when a task exists, the runtime enforces the guard conditions mentioned
577
607
above and releases the ` thread ` lock, allowing other tasks to start or make
578
608
progress.
@@ -641,17 +671,6 @@ implementation should be able to avoid separately allocating
641
671
` pending_sync_tasks ` by instead embedding a "next pending" linked list in the
642
672
` Subtask ` table element of the caller.
643
673
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
- ```
655
674
The first 3 fields of ` AsyncTask ` are simply immutable copies of
656
675
arguments/immediates passed to ` canon_lift ` that are used later on. The last 2
657
676
fields 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):
1952
1971
if not opts.callback:
1953
1972
[] = await call_and_trap_on_throw(callee, task, [])
1954
1973
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])
1959
1984
1960
1985
assert (opts.post_return is None )
1961
1986
task.exit()
@@ -1983,11 +2008,13 @@ allow the callee to reclaim any memory. An async call doesn't need a
1983
2008
1984
2009
Within the async case, there are two sub-cases depending on whether the
1985
2010
` 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).
1991
2018
1992
2019
Uncaught Core WebAssembly [ exceptions] result in a trap at component
1993
2020
boundaries. 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
2332
2359
coroutines are allowed to acquire the ` lock ` and execute.
2333
2360
``` python
2334
2361
async 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_()
2338
2364
return []
2339
2365
```
2340
2366
@@ -2415,6 +2441,7 @@ def canon_thread_hw_concurrency():
2415
2441
[ JavaScript Embedding ] : Explainer.md#JavaScript-embedding
2416
2442
[ Adapter Functions ] : FutureFeatures.md#custom-abis-via-adapter-functions
2417
2443
[ Shared-Everything Dynamic Linking ] : examples/SharedEverythingDynamicLinking.md
2444
+ [ Structured Concurrency ] : Async.md#structured-concurrency
2418
2445
2419
2446
[ Administrative Instructions ] : https://webassembly.github.io/spec/core/exec/runtime.html#syntax-instr-admin
2420
2447
[ Implementation Limits ] : https://webassembly.github.io/spec/core/appendix/implementation.html
0 commit comments