Skip to content

Commit 5f68460

Browse files
committed
Remove yielding behavior of waitable-set.poll to allow it to be called from synchronous functions
1 parent f85a97f commit 5f68460

File tree

6 files changed

+66
-110
lines changed

6 files changed

+66
-110
lines changed

design/mvp/CanonicalABI.md

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@ class WaitableSet:
767767
The `WaitableSet.drop` method traps if dropped while it still contains elements
768768
(whose `Waitable.wset` field would become dangling) or if it is being
769769
waited-upon by another `Task` (as indicated by the `num_waiting` field, which
770-
is incremented/decremented by `Task.{wait,poll}_for_event` below).
770+
is incremented/decremented by `Task.wait_until` below).
771771

772772
The `random.shuffle` in `get_pending_event` give embedders the semantic freedom
773773
to schedule delivery of events nondeterministically (e.g., taking into account
@@ -1042,31 +1042,10 @@ trap if another task tries to drop the waitable set being used.
10421042
return event
10431043
```
10441044

1045-
The `Task.poll_until` method is called by `canon_waitable_set_poll` and from
1046-
the event loop in `canon_lift` when `CallbackCode.POLL` is returned. Unlike
1047-
`wait_until`, `poll_until` does not wait for the given waitable set to have a
1048-
pending event, returning `EventCode.NONE` if there is none already. However,
1049-
`poll_until` *does* call `suspsend_until` to allow the runtime to
1050-
nondeterministically switch to another task (or not).
1051-
```python
1052-
def poll_until(self, ready_func, thread, wset, cancellable) -> Optional[EventTuple]:
1053-
assert(thread in self.threads and thread.task is self)
1054-
wset.num_waiting += 1
1055-
match self.suspend_until(ready_func, thread, cancellable):
1056-
case SuspendResult.CANCELLED:
1057-
event = (EventCode.TASK_CANCELLED, 0, 0)
1058-
case SuspendResult.NOT_CANCELLED:
1059-
if wset.has_pending_event():
1060-
event = wset.get_pending_event()
1061-
else:
1062-
event = (EventCode.NONE, 0, 0)
1063-
wset.num_waiting -= 1
1064-
return event
1065-
```
1066-
10671045
The `Task.yield_until` method is called by `canon_thread_yield` and from
1068-
the event loop in `canon_lift` when `CallbackCode.YIELD` is returned.
1069-
`yield_until` works like `poll_until` if given a fresh empty waitable set.
1046+
the event loop in `canon_lift` when `CallbackCode.YIELD` is returned and
1047+
calls `suspend_until` to allow the runtime to nondeterministically switch to
1048+
another task (or not).
10701049
```python
10711050
def yield_until(self, ready_func, thread, cancellable) -> EventTuple:
10721051
assert(thread in self.threads and thread.task is self)
@@ -3287,11 +3266,8 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
32873266
wset = inst.table.get(si)
32883267
trap_if(not isinstance(wset, WaitableSet))
32893268
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
3290-
case CallbackCode.POLL:
3291-
trap_if(not task.may_block())
3292-
wset = inst.table.get(si)
3293-
trap_if(not isinstance(wset, WaitableSet))
3294-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
3269+
case _:
3270+
trap()
32953271
thread.in_event_loop = False
32963272
inst.exclusive = True
32973273
event_code, p1, p2 = event
@@ -3300,17 +3276,17 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
33003276
task.exit()
33013277
return
33023278
```
3303-
The `Task.{wait,poll,yield}_until` methods called by the event loop are the
3304-
same methods called by the `yield`, `waitable-set.wait` and `waitable-set.poll`
3305-
built-ins. Thus, the main difference between stackful and stackless async is
3306-
whether these suspending operations are performed from an empty or non-empty
3307-
core wasm callstack (with the former allowing additional engine optimization).
3279+
The `Task.{wait,yield}_until` methods called by the event loop are the same
3280+
methods called by the `yield` and `waitable-set.wait` built-ins. Thus, the
3281+
main difference between stackful and stackless async is whether these
3282+
suspending operations are performed from an empty or non-empty core wasm
3283+
callstack (with the former allowing additional engine optimization).
33083284

33093285
If a `Task` is not allowed to block (because it was created for a non-`async`-
33103286
typed function call and has not yet returned a value), `YIELD` is always a
3311-
no-op and `WAIT` and `POLL` always trap. Thus, a component may implement a
3287+
no-op and `WAIT` always traps. Thus, a component may implement a
33123288
non-`async`-typed function with the `async callback` ABI, but the component
3313-
*must* call `task.return` *before* returning `WAIT` or `POLL`.
3289+
*must* call `task.return` *before* returning `WAIT`.
33143290

33153291
The event loop also releases `ComponentInstance.exclusive` (which was acquired
33163292
by `Task.enter` and will be released by `Task.exit`) before potentially
@@ -3346,8 +3322,7 @@ class CallbackCode(IntEnum):
33463322
EXIT = 0
33473323
YIELD = 1
33483324
WAIT = 2
3349-
POLL = 3
3350-
MAX = 3
3325+
MAX = 2
33513326

33523327
def unpack_callback_result(packed):
33533328
code = packed & 0xf
@@ -3357,7 +3332,7 @@ def unpack_callback_result(packed):
33573332
waitable_set_index = packed >> 4
33583333
return (CallbackCode(code), waitable_set_index)
33593334
```
3360-
The ability to asynchronously wait, poll, yield and exit is thus available to
3335+
The ability to asynchronously wait, yield and exit is thus available to
33613336
both the `callback` and non-`callback` cases, making `callback` just an
33623337
optimization to avoid allocating stacks for async languages that have avoided
33633338
the need for stackful coroutines by design (e.g., `async`/`await` in JS,
@@ -3888,26 +3863,22 @@ validation specifies:
38883863
* `$f` is given type `(func (param $si i32) (param $ptr i32) (result i32))`
38893864
* 🚟 - `cancellable` is allowed (otherwise it must be absent)
38903865

3891-
Calling `$f` invokes the following function, which returns `NONE` (`0`) instead
3892-
of blocking if there is no event available, and otherwise returns the event the
3893-
same way as `wait`.
3866+
Calling `$f` invokes the following function, which either returns an event that
3867+
was pending on one of the waitables in the given waitable set (the same way as
3868+
`waitable-set.wait`) or, if there is none, returns `0`.
38943869
```python
38953870
def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
38963871
trap_if(not thread.task.inst.may_leave)
3897-
trap_if(not thread.task.may_block())
38983872
wset = thread.task.inst.table.get(si)
38993873
trap_if(not isinstance(wset, WaitableSet))
3900-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
3874+
if thread.task.deliver_pending_cancel(cancellable):
3875+
event = (EventCode.TASK_CANCELLED, 0, 0)
3876+
elif not wset.has_pending_event():
3877+
event = (EventCode.NONE, 0, 0)
3878+
else:
3879+
event = wset.get_pending_event()
39013880
return unpack_event(mem, thread, ptr, event)
39023881
```
3903-
Even though `waitable-set.poll` doesn't block until the given waitable set has
3904-
a pending event, `poll_until` does transitively perform a `Thread.suspend`
3905-
which allows the embedder to nondeterministically switch to executing another
3906-
task (like `thread.yield`). To avoid encouraging spin-waiting and to support
3907-
hosts like browsers that require returning to the event loop for async I/O to
3908-
resolve, a non-`async`-typed function export that has not yet returned a value
3909-
unconditionally traps if it transitively attempts to call `poll`.
3910-
39113882
If `cancellable` is set, then `waitable-set.poll` will return whether the
39123883
supertask has already or concurrently requested cancellation.
39133884
`waitable-set.poll` (and other cancellable operations) will only indicate
@@ -3937,8 +3908,8 @@ def canon_waitable_set_drop(thread, i):
39373908
return []
39383909
```
39393910
Note that `WaitableSet.drop` will trap if it is non-empty or there is a
3940-
concurrent `waitable-set.wait` or `waitable-set.poll` or `async callback`
3941-
currently using this waitable set.
3911+
concurrent `waitable-set.wait` or `async callback` currently using this
3912+
waitable set.
39423913

39433914

39443915
### 🔀 `canon waitable.join`
@@ -4617,7 +4588,7 @@ def canon_thread_yield(cancellable, thread):
46174588
If a non-`async`-typed function export that has not yet returned a value
46184589
transitively calls `thread.yield`, it returns immediately without blocking
46194590
(instead of trapping, as with other possibly-blocking operations like
4620-
`waitable-set.poll`). This is because, unlike other built-ins, `thread.yield`
4591+
`waitable-set.wait`). This is because, unlike other built-ins, `thread.yield`
46214592
may be scattered liberally throughout code that might show up in the transitive
46224593
call tree of a synchronous function call.
46234594

design/mvp/Concurrency.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ all of which are described above or below in more detail:
474474
* cooperatively yielding (e.g., during a long-running computation) via the
475475
[`thread.yield`](#thread-built-ins) built-in
476476
* waiting for one of a set of concurrent operations to complete via the
477-
[`waitable-set.{wait,poll}`](#waitables-and-waitable-sets) built-ins
477+
[`waitable-set.wait`](#waitables-and-waitable-sets) built-in
478478
* waiting for a stream or future operation to complete via the
479479
[`{stream,future}.{,cancel-}{read,write}`](#streams-and-futures) built-ins
480480
* waiting for a subtask to cooperatively cancel itself via the
@@ -509,9 +509,8 @@ Specifically, waitable sets are created and used via the following built-ins:
509509
waitable set
510510
* [`waitable-set.wait`]: suspend until one of the waitables in the given set
511511
has a pending event and then return that event
512-
* [`waitable-set.poll`]: first `thread.yield` and, once resumed, if any of the
513-
waitables in the given set has a pending event, return that event; otherwise
514-
return a sentinel "none" value
512+
* [`waitable-set.poll`]: if any of the waitables in the given set has a pending
513+
event, return that event; otherwise return a sentinel "none" value
515514

516515
In addition to subtasks, (the readable and writable ends of) [streams and
517516
futures](#streams-and-futures) are *also* waitables, which means that a single
@@ -813,7 +812,7 @@ defined by the Component Model:
813812
waitable's event is delivered first.
814813
* If multiple threads wait on or poll the same waitable set at the same time,
815814
the distribution of events to threads is nondeterministic.
816-
* Whenever a thread yields or waits on (or polls) a waitable set with an already
815+
* Whenever a thread yields or waits on a waitable set with an already
817816
pending event, whether the thread suspends and transfers execution to an
818817
async caller is nondeterministic.
819818
* If multiple threads that previously suspended can be resumed at the same
@@ -1054,8 +1053,6 @@ The `(result i32)` lets the core function return what it wants the runtime to do
10541053
to run, but resuming thereafter without waiting on anything else.
10551054
* If the low 4 bits are `2`, the callee wants to wait for an event to occur in
10561055
the waitable set whose index is stored in the high 28 bits.
1057-
* If the low 4 bits are `3`, the callee wants to poll for any events that have
1058-
occurred in the waitable set whose index is stored in the high 28 bits.
10591056

10601057
When an async stackless function is exported, a companion "callback" function
10611058
must also be exported with signature:

design/mvp/Explainer.md

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,26 +1739,15 @@ For details, see [Waitables and Waitable Sets] in the concurrency explainer and
17391739

17401740
where `event` is defined as in [`waitable-set.wait`](#-waitable-setwait).
17411741

1742-
The `waitable-set.poll` built-in suspends the [current thread] in
1743-
the "ready" state (like `thread.yield`). Once nondeterministically resumed,
1744-
`waitable-set.poll` will return either an event from one of the waitables in
1745-
`s` or, if there is none, the `none` `event`. Thus, repeatedly calling
1746-
`waitable-set.poll` in a loop allows other tasks to execute.
1742+
The `waitable-set.poll` built-in returns either an event from one of the
1743+
waitables in `s` or, if there is none, the `none` `event`.
17471744

17481745
If `cancellable` is set, `waitable-set.poll` may return `task-cancelled`
17491746
(`6`) if the caller requests [cancellation] of the [current task]. If
17501747
`cancellable` is not set, `task-cancelled` is never returned.
17511748
`task-cancelled` is returned at most once for a given task and thus must be
17521749
propagated once received.
17531750

1754-
If `waitable-set.poll` is called from a synchronous- or `async callback`-lifted
1755-
export, no other threads that were implicitly created by a separate
1756-
synchronous- or `async callback`-lifted export call can start or progress in
1757-
the current component instance until `waitable-set.poll` returns (thereby
1758-
ensuring non-reentrance of the core wasm code). However, explicitly-created
1759-
threads and threads implicitly created by non-`callback` `async`-lifted
1760-
("stackful async") exports may start or progress at any time.
1761-
17621751
The Canonical ABI of `waitable-set.poll` is the same as `waitable-set.wait`
17631752
(with the `none` case indicated by returning `0`).
17641753

design/mvp/canonical-abi/definitions.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -651,20 +651,6 @@ def ready_and_has_event():
651651
wset.num_waiting -= 1
652652
return event
653653

654-
def poll_until(self, ready_func, thread, wset, cancellable) -> Optional[EventTuple]:
655-
assert(thread in self.threads and thread.task is self)
656-
wset.num_waiting += 1
657-
match self.suspend_until(ready_func, thread, cancellable):
658-
case SuspendResult.CANCELLED:
659-
event = (EventCode.TASK_CANCELLED, 0, 0)
660-
case SuspendResult.NOT_CANCELLED:
661-
if wset.has_pending_event():
662-
event = wset.get_pending_event()
663-
else:
664-
event = (EventCode.NONE, 0, 0)
665-
wset.num_waiting -= 1
666-
return event
667-
668654
def yield_until(self, ready_func, thread, cancellable) -> EventTuple:
669655
assert(thread in self.threads and thread.task is self)
670656
match self.suspend_until(ready_func, thread, cancellable):
@@ -2047,11 +2033,8 @@ def thread_func(thread):
20472033
wset = inst.table.get(si)
20482034
trap_if(not isinstance(wset, WaitableSet))
20492035
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2050-
case CallbackCode.POLL:
2051-
trap_if(not task.may_block())
2052-
wset = inst.table.get(si)
2053-
trap_if(not isinstance(wset, WaitableSet))
2054-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2036+
case _:
2037+
trap()
20552038
thread.in_event_loop = False
20562039
inst.exclusive = True
20572040
event_code, p1, p2 = event
@@ -2068,8 +2051,7 @@ class CallbackCode(IntEnum):
20682051
EXIT = 0
20692052
YIELD = 1
20702053
WAIT = 2
2071-
POLL = 3
2072-
MAX = 3
2054+
MAX = 2
20732055

20742056
def unpack_callback_result(packed):
20752057
code = packed & 0xf
@@ -2283,10 +2265,14 @@ def unpack_event(mem, thread, ptr, e: EventTuple):
22832265

22842266
def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
22852267
trap_if(not thread.task.inst.may_leave)
2286-
trap_if(not thread.task.may_block())
22872268
wset = thread.task.inst.table.get(si)
22882269
trap_if(not isinstance(wset, WaitableSet))
2289-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
2270+
if thread.task.deliver_pending_cancel(cancellable):
2271+
event = (EventCode.TASK_CANCELLED, 0, 0)
2272+
elif not wset.has_pending_event():
2273+
event = (EventCode.NONE, 0, 0)
2274+
else:
2275+
event = wset.get_pending_event()
22902276
return unpack_event(mem, thread, ptr, event)
22912277

22922278
### 🔀 `canon waitable-set.drop`

design/mvp/canonical-abi/run_tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,8 @@ def consumer(thread, args):
10311031

10321032
remain = [subi1, subi2]
10331033
while remain:
1034+
[ret] = canon_thread_yield(True, thread)
1035+
assert(ret == 0)
10341036
retp = 8
10351037
[event] = canon_waitable_set_poll(True, consumer_heap.memory, thread, seti, retp)
10361038
if event == EventCode.NONE:

test/async/trap-if-block-and-sync.wast

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,20 @@
7575
(i32.const 2 (; WAIT ;))
7676
(i32.shl (call $waitable-set.new) (i32.const 4)))
7777
)
78-
(func (export "trap-if-poll")
79-
(call $waitable-set.poll (call $waitable-set.new) (i32.const 0xdeadbeef))
80-
unreachable
78+
(func (export "poll-is-fine") (result i32)
79+
(local $ret i32)
80+
(local.set $ret (call $waitable-set.poll (call $waitable-set.new) (i32.const 0)))
81+
(if (i32.ne (i32.const 0 (; NONE ;)) (local.get $ret))
82+
(then unreachable))
83+
(if (i32.ne (i32.const 0) (i32.load (i32.const 0)))
84+
(then unreachable))
85+
(if (i32.ne (i32.const 0) (i32.load (i32.const 4)))
86+
(then unreachable))
87+
(i32.const 42)
8188
)
82-
(func (export "trap-if-poll-cb") (result i32)
89+
(func (export "trap-if-invalid-callback-code") (param $invalid-code i32) (result i32)
8390
(i32.or
84-
(i32.const 3 (; POLL ;))
91+
(local.get $invalid-code)
8592
(i32.shl (call $waitable-set.new) (i32.const 4)))
8693
)
8794
(func (export "yield-is-fine") (result i32)
@@ -174,8 +181,8 @@
174181
(func (export "trap-if-suspend") (canon lift (core func $core "trap-if-suspend")))
175182
(func (export "trap-if-wait") (canon lift (core func $core "trap-if-wait")))
176183
(func (export "trap-if-wait-cb") (canon lift (core func $core "trap-if-wait-cb") async (callback (func $core "unreachable-cb"))))
177-
(func (export "trap-if-poll") (canon lift (core func $core "trap-if-poll")))
178-
(func (export "trap-if-poll-cb") (canon lift (core func $core "trap-if-poll-cb") async (callback (func $core "unreachable-cb"))))
184+
(func (export "poll-is-fine") (result u32) (canon lift (core func $core "poll-is-fine")))
185+
(func (export "trap-if-invalid-callback-code") (param "invalid-code" u32) (canon lift (core func $core "trap-if-invalid-callback-code") async (callback (func $core "unreachable-cb"))))
179186
(func (export "yield-is-fine") (result u32) (canon lift (core func $core "yield-is-fine")))
180187
(func (export "yield-is-fine-cb") (result u32) (canon lift (core func $core "yield-is-fine-cb") async (callback (func $core "return-42-cb"))))
181188
(func (export "trap-if-sync-call-async1") (canon lift (core func $core "trap-if-sync-call-async1")))
@@ -197,8 +204,8 @@
197204
(func (export "trap-if-suspend") (alias export $d "trap-if-suspend"))
198205
(func (export "trap-if-wait") (alias export $d "trap-if-wait"))
199206
(func (export "trap-if-wait-cb") (alias export $d "trap-if-wait-cb"))
200-
(func (export "trap-if-poll") (alias export $d "trap-if-poll"))
201-
(func (export "trap-if-poll-cb") (alias export $d "trap-if-poll-cb"))
207+
(func (export "poll-is-fine") (alias export $d "poll-is-fine"))
208+
(func (export "trap-if-invalid-callback-code") (alias export $d "trap-if-invalid-callback-code"))
202209
(func (export "yield-is-fine") (alias export $d "yield-is-fine"))
203210
(func (export "yield-is-fine-cb") (alias export $d "yield-is-fine-cb"))
204211
(func (export "trap-if-sync-cancel") (alias export $d "trap-if-sync-cancel"))
@@ -223,9 +230,13 @@
223230
(component instance $i $Tester)
224231
(assert_trap (invoke "trap-if-wait-cb") "cannot block a synchronous task before returning")
225232
(component instance $i $Tester)
226-
(assert_trap (invoke "trap-if-poll") "cannot block a synchronous task before returning")
233+
(assert_return (invoke "poll-is-fine") (u32.const 42))
234+
(component instance $i $Tester)
235+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 3)) "unsupported callback code: 3")
236+
(component instance $i $Tester)
237+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 4)) "unsupported callback code: 4")
227238
(component instance $i $Tester)
228-
(assert_trap (invoke "trap-if-poll-cb") "cannot block a synchronous task before returning")
239+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 15)) "unsupported callback code: 15")
229240
(component instance $i $Tester)
230241
(assert_return (invoke "yield-is-fine") (u32.const 42))
231242
(component instance $i $Tester)

0 commit comments

Comments
 (0)