@@ -134,16 +134,16 @@ class Store:
134
134
thread.resume()
135
135
return
136
136
```
137
- The ` tick ` method does not have an analogue in Core WebAssembly and enables
138
- [ native async] ( Async.md ) support in the Component Model. The expectation is
139
- that the host will interleave calls to ` invoke ` with calls to ` tick ` ,
140
- repeatedly calling ` tick ` until there is no more work to do or the store is
141
- destroyed. The nondeterministic ` random.shuffle ` indicates that the embedder is
142
- allowed to use any algorithm (involving priorities, fairness, etc) to choose
143
- which thread to schedule next (and hopefully an algorithm more efficient than
144
- the simple polling loop written above). The ` Thread.ready ` and ` Thread.resume `
145
- methods along with how the ` pending ` list is populated are all defined
146
- [ below] ( #thread-state ) as part of the ` Thread ` class.
137
+ The ` Store. tick` method does not have an analogue in Core WebAssembly and
138
+ enables [ native async support ] ( Async.md ) in the Component Model. The
139
+ expectation is that the host will interleave calls to ` invoke ` with calls to
140
+ ` tick ` , repeatedly calling ` tick ` until there is no more work to do or the
141
+ store is destroyed. The nondeterministic ` random.shuffle ` indicates that the
142
+ embedder is allowed to use any algorithm (involving priorities, fairness, etc)
143
+ to choose which thread to schedule next (and hopefully an algorithm more
144
+ efficient than the simple polling loop written above). The ` Thread.ready ` and
145
+ ` Thread.resume ` methods along with how the ` pending ` list is populated are all
146
+ defined [ below] ( #thread-state ) as part of the ` Thread ` class.
147
147
148
148
The ` FuncInst ` passed to ` Store.invoke ` is defined to take 3 parameters:
149
149
* an optional ` caller ` ` Supertask ` which is used to maintain the
@@ -452,14 +452,12 @@ threads; at that point `Thread`s and `Task`s will be many-to-one, with a single
452
452
453
453
` Thread ` is implemented using the Python standard library's [ ` threading ` ]
454
454
module. While a Python [ ` threading.Thread ` ] is a preemptively-scheduled [ kernel
455
- thread] , the ` Thread ` abstraction defined here is a cooperatively-scheduled
456
- user-space thread that only switches between ` Thread ` s when one of the ` Thread `
457
- methods is called. To implement cooperativity, the code below uses
458
- [ ` threading.Lock ` ] to control and serialize execution. If Python had [ fibers]
459
- or algebraic effects, those could have been used instead since all that's
460
- needed is the ability to switch stacks. In any case, the use of
461
- ` threading.Thread ` is encapsulated by the ` Thread ` class so that the rest of
462
- the Canonical ABI can simply use ` suspend ` /` resume ` .
455
+ thread] , it is coerced to behave like a cooperatively-scheduled [ fiber] by
456
+ careful use of [ ` threading.Lock ` ] . If Python had built-in fibers (or algebraic
457
+ effects), those could have been used instead since all that's needed is the
458
+ ability to switch stacks. In any case, the use of ` threading.Thread ` is
459
+ encapsulated by the ` Thread ` class so that the rest of the Canonical ABI can
460
+ simply use ` suspend ` , ` resume ` , etc.
463
461
464
462
Introducing the ` Thread ` class in chunks, a ` Thread ` has the following fields
465
463
and can be in one of the following 3 states based on these fields:
@@ -607,9 +605,8 @@ consider the call `BLOCKED` or keep going.
607
605
608
606
A "waitable" is a concurrent activity that can be waited on by the built-ins
609
607
` waitable-set.wait ` and ` waitable-set.poll ` . Currently, there are 5 different
610
- kinds of waitables: [ subtasks] ( Async.md#structured-concurrency )
611
- and the 4 combinations of the [ readable and writable ends of futures and
612
- streams] ( Async.md#streams-and-futures ) .
608
+ kinds of waitables: [ subtasks] and the 4 combinations of the [ readable and
609
+ writable ends] of futures and streams.
613
610
614
611
Waitables deliver "events" which are values of the following ` EventTuple ` type.
615
612
The two ` int ` "payload" fields of ` EventTuple ` store core wasm ` i32 ` s and are
@@ -764,15 +761,32 @@ class Task(Call, Supertask):
764
761
self .context = ContextLocalStorage()
765
762
```
766
763
764
+ The ` thread ` field is initialized by ` Task.thread_start ` , which is called by
765
+ ` Thread ` 's constructor. Symmetrically, when the ` Thread ` 's root function
766
+ call returns, ` Task.thread_stop ` is called to trap if the ` OnResolve ` callback
767
+ has not been called (by the ` Task.return_ ` and ` Task.cancel ` methods,
768
+ defined below).
769
+ ``` python
770
+ def thread_start (self , thread ):
771
+ assert (self .thread is None and thread.task is self )
772
+ self .thread = thread
773
+
774
+ def thread_stop (self , thread ):
775
+ assert (thread is self .thread and thread.task is self )
776
+ self .thread = None
777
+ trap_if(self .state != Task.State.RESOLVED )
778
+ assert (self .num_borrows == 0 )
779
+ ```
780
+
767
781
The ` Task.trap_if_on_the_stack ` method checks for unintended reentrance,
768
- enforcing a [ component invariant] . This guard uses the ` supertask ` field of
769
- ` Task ` which points to the task's supertask in the async call tree defined by
770
- [ structured concurrency] . Structured concurrency is necessary to distinguish
771
- between the deadlock-hazardous kind of reentrance (where the new task is a
772
- transitive subtask of a task already running in the same component instance)
773
- and the normal kind of async reentrance (where the new task is just a sibling
774
- of any existing tasks running in the component instance). Note that, in the
775
- [ future] ( Async.md#TODO ) , there will be a way for a function to opt in (via
782
+ enforcing a [ component invariant] . This guard uses the ` Supertask ` defined by
783
+ the [ Embedding ] ( #embedding ) interface to walk up the async call tree defined as
784
+ part of [ structured concurrency] . The async call tree is necessary to
785
+ distinguish between the deadlock-hazardous kind of reentrance (where the new
786
+ task is a transitive subtask of a task already running in the same component
787
+ instance) and the normal kind of async reentrance (where the new task is just a
788
+ sibling of any existing tasks running in the component instance). Note that, in
789
+ the [ future] ( Async.md#TODO ) , there will be a way for a function to opt in (via
776
790
function type attribute) to the hazardous kind of reentrance, which will nuance
777
791
this test.
778
792
``` python
@@ -800,8 +814,8 @@ indicate that the core wasm being executed does not expect to be reentered
800
814
(e.g., because the code is using a single global linear memory shadow stack).
801
815
Concretely, this is assumed to be the case when core wasm is lifted
802
816
synchronously or with ` async callback ` . This predicate is used by the other
803
- ` Task ` methods to determine whether to acquire/release the
804
- component-instance-wide ` exclusive ` [ ` asyncio.Lock ` ] .
817
+ ` Task ` methods to determine whether to acquire/release the component instance's
818
+ ` exclusive ` lock .
805
819
``` python
806
820
def needs_exclusive (self ):
807
821
return self .opts.sync or self .opts.callback
@@ -826,6 +840,7 @@ backpressure is disabled. There are three sources of backpressure:
826
840
827
841
``` python
828
842
def enter (self ):
843
+ assert (self .thread is not None )
829
844
def has_backpressure ():
830
845
return self .inst.backpressure or (self .needs_exclusive() and self .inst.exclusive)
831
846
if has_backpressure() or self .inst.num_waiting_to_enter > 0 :
@@ -851,6 +866,17 @@ order. Additionally, the above definition ensures the following properties:
851
866
backpressure (i.e., disabling backpressure never unleashes an unstoppable
852
867
thundering heard of pending tasks).
853
868
869
+ Symmetrically, the ` Task.exit ` method is called before a ` Task ` 's ` Thread `
870
+ returns to clear the ` exclusive ` flag set by ` Task.enter ` , allowing other
871
+ ` needs_exclusive ` tasks to start or make progress:
872
+ ``` python
873
+ def exit (self ):
874
+ assert (self .thread is not None )
875
+ if self .needs_exclusive():
876
+ assert (self .inst.exclusive)
877
+ self .inst.exclusive = False
878
+ ```
879
+
854
880
The ` Task.request_cancellation ` method is called by the host or wasm caller
855
881
(via the ` Call ` interface of ` Task ` ) to signal that they don't need the return
856
882
value and that the caller should hurry up and call the ` OnResolve ` callback. If
@@ -860,7 +886,7 @@ called with `cancellable` set), `request_cancellation` immediately resumes the
860
886
thread, giving the thread the chance to handle cancellation promptly (allowing
861
887
` subtask.cancel ` to complete eagerly without returning ` BLOCKED ` ). Otherwise,
862
888
the cancellation request is remembered in the ` Task ` 's ` state ` so that it can
863
- be delivered in the future by ` Task.wait_until ` (defined next).
889
+ be delivered in the future by ` Task.suspend_until ` (defined next).
864
890
``` python
865
891
def request_cancellation (self ):
866
892
assert (self .state == Task.State.INITIAL )
@@ -890,13 +916,12 @@ pending cancellation set by `Task.request_cancellation`:
890
916
return self .thread.suspend_until(ready_func, cancellable)
891
917
```
892
918
893
- The ` Task.wait_until ` method is called by ` waitable-set.wait ` or from the event
894
- loop of an ` async callback ` function when ` CallbackCode.WAIT ` is returned.
919
+ The ` Task.wait_until ` method is called by ` canon_waitable_set_wait ` and from
920
+ the event loop in ` canon_lift ` when ` CallbackCode.WAIT ` is returned.
895
921
` wait_until ` waits until a waitable in the given waitable set has a pending
896
922
event to deliver * and* the caller-supplied condition is met. While suspended,
897
- the ` WaitableSet.num_waiting ` counter is kept above ` 0 ` so that
898
- ` waitable-set.drop ` will trap if another task tries to drop the waitable set
899
- being used.
923
+ the ` num_waiting ` counter is kept above ` 0 ` so that ` waitable-set.drop ` will
924
+ trap if another task tries to drop the waitable set being used.
900
925
``` python
901
926
def wait_until (self , ready_func , wset , cancellable ) -> EventTuple:
902
927
wset.num_waiting += 1
@@ -910,11 +935,11 @@ being used.
910
935
return event
911
936
```
912
937
913
- The ` Task.poll_until ` method is called by ` waitable-set.poll ` or from the event
914
- loop of an ` async callback ` function when ` CallbackCode.POLL ` is returned.
915
- Unlike ` wait_until ` , ` poll_until ` does not wait for the given waitable set to
916
- have a pending event, returning ` EventCode.NONE ` if there is none already.
917
- However, ` poll_until ` * does* call ` suspsend_until ` to allow the runtime to
938
+ The ` Task.poll_until ` method is called by ` canon_waitable_set_poll ` and from
939
+ the event loop in ` canon_lift ` when ` CallbackCode.POLL ` is returned. Unlike
940
+ ` wait_until ` , ` poll_until ` does not wait for the given waitable set to have a
941
+ pending event, returning ` EventCode.NONE ` if there is none already. However,
942
+ ` poll_until ` * does* call ` suspsend_until ` to allow the runtime to
918
943
nondeterministically switch to another task (or not).
919
944
``` python
920
945
def poll_until (self , ready_func , wset , cancellable ) -> Optional[EventTuple]:
@@ -929,10 +954,9 @@ nondeterministically switch to another task (or not).
929
954
return event
930
955
```
931
956
932
- The ` Task.yield_until ` method is called by the ` yield ` built-in or from the
933
- event loop of an ` async callback ` function when ` CallbackCode.YIELD ` is
934
- returned. ` yield_until ` works like ` poll_until ` if given a fresh empty waitable
935
- set.
957
+ The ` Task.yield_until ` method is called by ` canon_yield ` and from
958
+ the event loop in ` canon_lift ` when ` CallbackCode.YIELD ` is returned.
959
+ ` yield_until ` works like ` poll_until ` if given a fresh empty waitable set.
936
960
``` python
937
961
def yield_until (self , ready_func , cancellable ) -> EventTuple:
938
962
if not self .suspend_until(ready_func, cancellable):
@@ -941,13 +965,13 @@ set.
941
965
return (EventCode.NONE , 0 , 0 )
942
966
```
943
967
944
- The ` Task.return_ ` method is called by either ` canon_task_return ` or
945
- ` canon_lift ` to return a list of lifted values to the task's caller via the
946
- ` OnResolve ` callback. There is a dynamic error if the callee has not dropped
947
- all borrowed handles by the time ` task.return ` is called which means that the
948
- caller can assume that all its lent handles have been returned to it when it
949
- receives the ` SUBTASK ` ` RETURNED ` event. Note that the initial ` trap_if ` allows
950
- a task to return a value even after cancellation has been requested.
968
+ The ` Task.return_ ` method is called by ` canon_task_return ` and ` canon_lift ` to
969
+ return a list of lifted values to the task's caller via the ` OnResolve `
970
+ callback. There is a dynamic error if the callee has not dropped all borrowed
971
+ handles by the time ` task.return ` is called which means that the caller can
972
+ assume that all its lent handles have been returned to it when it receives the
973
+ ` SUBTASK ` ` RETURNED ` event. Note that the initial ` trap_if ` allows a task to
974
+ return a value even after cancellation has been requested.
951
975
``` python
952
976
def return_ (self , result ):
953
977
trap_if(self .state == Task.State.RESOLVED )
@@ -957,13 +981,14 @@ a task to return a value even after cancellation has been requested.
957
981
self .state = Task.State.RESOLVED
958
982
```
959
983
960
- The ` Task.cancel ` method is called by ` canon_task_cancel ` and enforces the same
961
- ` num_borrows ` condition as ` return_ ` , ensuring that when the caller's
962
- ` OnResolve ` callback is called, the caller knows all borrows have been
963
- returned. The initial ` trap_if ` only allows cancellation after cancellation has
964
- been * delivered* to core wasm. In particular, if ` request_cancellation ` cannot
965
- synchronously deliver cancellation and sets ` Task.state ` to ` PENDING_CANCEL ` ,
966
- core wasm will still trap if it tries to call ` task.cancel ` .
984
+ Lastly, the ` Task.cancel ` method is called by ` canon_task_cancel ` and
985
+ enforces the same ` num_borrows ` condition as ` return_ ` , ensuring that when
986
+ the caller's ` OnResolve ` callback is called, the caller knows all borrows
987
+ have been returned. The initial ` trap_if ` only allows cancellation after
988
+ cancellation has been * delivered* to core wasm. In particular, if
989
+ ` request_cancellation ` cannot synchronously deliver cancellation and sets
990
+ ` Task.state ` to ` PENDING_CANCEL ` , core wasm will still trap if it tries to
991
+ call ` task.cancel ` .
967
992
``` python
968
993
def cancel (self ):
969
994
trap_if(self .state != Task.State.CANCEL_DELIVERED )
@@ -972,33 +997,6 @@ core wasm will still trap if it tries to call `task.cancel`.
972
997
self .state = Task.State.RESOLVED
973
998
```
974
999
975
- The ` Task.exit ` method is called before a ` Task ` 's ` Thread ` returns to clear
976
- the ` exclusive ` flag set by ` Task.enter ` , allowing other ` needs_exclusive `
977
- tasks to start or make progress.
978
- ``` python
979
- def exit (self ):
980
- assert (self .thread is not None )
981
- if self .needs_exclusive():
982
- assert (self .inst.exclusive)
983
- self .inst.exclusive = False
984
- ```
985
-
986
- Lastly, the ` Task.thread_start ` and ` Task.thread_stop ` functions are called by
987
- a ` Thread ` (defined above) to register/unregister itself when it starts/stops.
988
- When a ` Task ` 's final (and, currently, only) ` Thread ` returns, the ` trap_if `
989
- guards that the task has upheld its contract to call ` OnResolve ` .
990
- ``` python
991
- def thread_start (self , thread ):
992
- assert (self .thread is None and thread.task is self )
993
- self .thread = thread
994
-
995
- def thread_stop (self , thread ):
996
- assert (thread is self .thread and thread.task is self )
997
- self .thread = None
998
- trap_if(self .state != Task.State.RESOLVED )
999
- assert (self .num_borrows == 0 )
1000
- ```
1001
-
1002
1000
1003
1001
#### Subtask State
1004
1002
@@ -1198,15 +1196,15 @@ and lowering and defined below.
1198
1196
1199
1197
Values of ` stream ` type are represented in the Canonical ABI as ` i32 ` indices
1200
1198
into the current component instance's table referring to either the
1201
- [ readable or writable end] ( Async.md#streams-and-futures ) of a stream. Reading
1202
- from the readable end of a stream is achieved by calling ` stream.read ` and
1203
- supplying a ` WritableBuffer ` . Conversely, writing to the writable end of a
1204
- stream is achieved by calling ` stream.write ` and supplying a ` ReadableBuffer ` .
1205
- The runtime waits until both a readable and writable buffer have been supplied
1206
- and then performs a direct copy between the two buffers. This rendezvous-based
1207
- design avoids the need for an intermediate buffer and copy (unlike, e.g., a
1208
- Unix pipe; a Unix pipe would instead be implemented as a resource type owning
1209
- the buffer memory and * two* streams; on going in and one coming out).
1199
+ [ readable or writable end] of a stream. Reading from the readable end of a
1200
+ stream is achieved by calling ` stream.read ` and supplying a ` WritableBuffer ` .
1201
+ Conversely, writing to the writable end of a stream is achieved by calling
1202
+ ` stream.write ` and supplying a ` ReadableBuffer ` . The runtime waits until both
1203
+ a readable and writable buffer have been supplied and then performs a direct
1204
+ copy between the two buffers. This rendezvous-based design avoids the need
1205
+ for an intermediate buffer and copy (unlike, e.g., a Unix pipe; a Unix pipe
1206
+ would instead be implemented as a resource type owning the buffer memory and
1207
+ * two* streams; on going in and one coming out).
1210
1208
1211
1209
The result of a ` {stream,future}.{read,write} ` is communicated to the wasm
1212
1210
guest via a ` CopyResult ` code:
@@ -3771,8 +3769,8 @@ def canon_waitable_set_drop(task, i):
3771
3769
return []
3772
3770
```
3773
3771
Note that ` WaitableSet.drop ` will trap if it is non-empty or there is a
3774
- concurrent ` yield ` , ` waitable-set.wait ` , ` waitable-set.poll ` or `async
3775
- callback` currently using this waitable set.
3772
+ concurrent ` waitable-set.wait ` or ` waitable-set.poll ` or ` async callback `
3773
+ currently using this waitable set.
3776
3774
3777
3775
3778
3776
### 🔀 ` canon waitable.join `
@@ -4438,7 +4436,9 @@ def canon_thread_available_parallelism():
4438
4436
[ Structured Concurrency ] : Async.md#structured-concurrency
4439
4437
[ Backpressure ] : Async.md#backpressure
4440
4438
[ Current Task ] : Async.md#current-task
4439
+ [ Subtasks ] : Async.md#structured-concurrency
4441
4440
[ Readable and Writable Ends ] : Async.md#streams-and-futures
4441
+ [ Readable or Writable End ] : Async.md#streams-and-futures
4442
4442
[ Context-Local Storage ] : Async.md#context-local-storage
4443
4443
[ Subtask State Machine ] : Async.md#cancellation
4444
4444
[ Lazy Lowering ] : https://github.com/WebAssembly/component-model/issues/383
@@ -4473,7 +4473,7 @@ def canon_thread_available_parallelism():
4473
4473
[ Surrogate ] : https://unicode.org/faq/utf_bom.html#utf16-2
4474
4474
[ Name Mangling ] : https://en.wikipedia.org/wiki/Name_mangling
4475
4475
[ Kernel Thread ] : https://en.wikipedia.org/wiki/Thread_(computing)#kernel_thread
4476
- [ Fibers ] : https://en.wikipedia.org/wiki/Fiber_(computer_science)
4476
+ [ Fiber ] : https://en.wikipedia.org/wiki/Fiber_(computer_science)
4477
4477
[ Asyncify ] : https://emscripten.org/docs/porting/asyncify.html
4478
4478
4479
4479
[ `import_name` ] : https://clang.llvm.org/docs/AttributeReference.html#import-name
0 commit comments