@@ -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
@@ -1950,10 +1969,16 @@ async def canon_lift(opts, inst, callee, ft, caller, start_thunk, return_thunk):
1950
1969
if not opts.callback:
1951
1970
[] = await call_and_trap_on_throw(callee, task, [])
1952
1971
else :
1953
- [ctx] = await call_and_trap_on_throw(callee, task, [])
1954
- while ctx != 0 :
1955
- event, payload = await task.wait()
1956
- [ctx] = await call_and_trap_on_throw(opts.callback, task, [ctx, event, payload])
1972
+ [packed_ctx] = await call_and_trap_on_throw(callee, task, [])
1973
+ while packed_ctx != 0 :
1974
+ is_yield = bool (packed_ctx & 1 )
1975
+ ctx = packed_ctx & ~ 1
1976
+ if is_yield:
1977
+ await task.yield_()
1978
+ event, payload = (EventCode.YIELDED , 0 )
1979
+ else :
1980
+ event, payload = await task.wait()
1981
+ [packed_ctx] = await call_and_trap_on_throw(opts.callback, task, [ctx, event, payload])
1957
1982
1958
1983
assert (opts.post_return is None )
1959
1984
task.exit()
@@ -1981,11 +2006,13 @@ allow the callee to reclaim any memory. An async call doesn't need a
1981
2006
1982
2007
Within the async case, there are two sub-cases depending on whether the
1983
2008
` callback ` ` canonopt ` was set. When ` callback ` is present, waiting happens in
1984
- an "event loop" inside ` canon_lift ` . Otherwise, waiting must happen by calling
1985
- ` task.wait ` (defined below), which potentially requires the runtime
1986
- implementation to use a fiber (aka. stackful coroutine) to switch to another
1987
- task. Thus, ` callback ` is an optimization for avoiding fiber creation for async
1988
- languages that don't need it (e.g., JS, Python, C# and Rust).
2009
+ an "event loop" inside ` canon_lift ` which also allows yielding (i.e., allowing
2010
+ other tasks to run without blocking) by setting the LSB of the returned ` i32 ` .
2011
+ Otherwise, waiting must happen by calling ` task.wait ` (defined below), which
2012
+ potentially requires the runtime implementation to use a fiber (aka. stackful
2013
+ coroutine) to switch to another task. Thus, ` callback ` is an optimization for
2014
+ avoiding fiber creation for async languages that don't need it (e.g., JS,
2015
+ Python, C# and Rust).
1989
2016
1990
2017
Uncaught Core WebAssembly [ exceptions] result in a trap at component
1991
2018
boundaries. Thus, if a component wishes to signal an error, it must use some
@@ -2330,9 +2357,8 @@ Python `asyncio.sleep(0)` in the middle to make it clear that other
2330
2357
coroutines are allowed to acquire the ` lock ` and execute.
2331
2358
``` python
2332
2359
async def canon_task_yield (task ):
2333
- task.inst.thread.release()
2334
- await asyncio.sleep(0 )
2335
- await task.inst.thread.acquire()
2360
+ trap_if(task.opts.callback is not None )
2361
+ await task.yield_()
2336
2362
return []
2337
2363
```
2338
2364
@@ -2413,6 +2439,7 @@ def canon_thread_hw_concurrency():
2413
2439
[ JavaScript Embedding ] : Explainer.md#JavaScript-embedding
2414
2440
[ Adapter Functions ] : FutureFeatures.md#custom-abis-via-adapter-functions
2415
2441
[ Shared-Everything Dynamic Linking ] : examples/SharedEverythingDynamicLinking.md
2442
+ [ Structured Concurrency ] : Async.md#structured-concurrency
2416
2443
2417
2444
[ Administrative Instructions ] : https://webassembly.github.io/spec/core/exec/runtime.html#syntax-instr-admin
2418
2445
[ Implementation Limits ] : https://webassembly.github.io/spec/core/appendix/implementation.html
0 commit comments