Skip to content

Commit 34ed26f

Browse files
committed
Remove yielding behavior of waitable-set.poll to allow it to be called from synchronous functions
1 parent 93d0588 commit 34ed26f

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)
@@ -3277,11 +3256,8 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
32773256
wset = inst.table.get(si)
32783257
trap_if(not isinstance(wset, WaitableSet))
32793258
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
3280-
case CallbackCode.POLL:
3281-
trap_if(not task.may_block())
3282-
wset = inst.table.get(si)
3283-
trap_if(not isinstance(wset, WaitableSet))
3284-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
3259+
case _:
3260+
trap()
32853261
thread.in_event_loop = False
32863262
inst.exclusive = True
32873263
event_code, p1, p2 = event
@@ -3290,17 +3266,17 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
32903266
task.exit()
32913267
return
32923268
```
3293-
The `Task.{wait,poll,yield}_until` methods called by the event loop are the
3294-
same methods called by the `yield`, `waitable-set.wait` and `waitable-set.poll`
3295-
built-ins. Thus, the main difference between stackful and stackless async is
3296-
whether these suspending operations are performed from an empty or non-empty
3297-
core wasm callstack (with the former allowing additional engine optimization).
3269+
The `Task.{wait,yield}_until` methods called by the event loop are the same
3270+
methods called by the `yield` and `waitable-set.wait` built-ins. Thus, the
3271+
main difference between stackful and stackless async is whether these
3272+
suspending operations are performed from an empty or non-empty core wasm
3273+
callstack (with the former allowing additional engine optimization).
32983274

32993275
If a `Task` is not allowed to block (because it was created for a non-`async`-
33003276
typed function call and has not yet returned a value), `YIELD` is always a
3301-
no-op and `WAIT` and `POLL` always trap. Thus, a component may implement a
3277+
no-op and `WAIT` always traps. Thus, a component may implement a
33023278
non-`async`-typed function with the `async callback` ABI, but the component
3303-
*must* call `task.return` *before* returning `WAIT` or `POLL`.
3279+
*must* call `task.return` *before* returning `WAIT`.
33043280

33053281
The event loop also releases `ComponentInstance.exclusive` (which was acquired
33063282
by `Task.enter` and will be released by `Task.exit`) before potentially
@@ -3336,8 +3312,7 @@ class CallbackCode(IntEnum):
33363312
EXIT = 0
33373313
YIELD = 1
33383314
WAIT = 2
3339-
POLL = 3
3340-
MAX = 3
3315+
MAX = 2
33413316

33423317
def unpack_callback_result(packed):
33433318
code = packed & 0xf
@@ -3347,7 +3322,7 @@ def unpack_callback_result(packed):
33473322
waitable_set_index = packed >> 4
33483323
return (CallbackCode(code), waitable_set_index)
33493324
```
3350-
The ability to asynchronously wait, poll, yield and exit is thus available to
3325+
The ability to asynchronously wait, yield and exit is thus available to
33513326
both the `callback` and non-`callback` cases, making `callback` just an
33523327
optimization to avoid allocating stacks for async languages that have avoided
33533328
the need for stackful coroutines by design (e.g., `async`/`await` in JS,
@@ -3878,26 +3853,22 @@ validation specifies:
38783853
* `$f` is given type `(func (param $si i32) (param $ptr i32) (result i32))`
38793854
* 🚟 - `cancellable` is allowed (otherwise it must be absent)
38803855

3881-
Calling `$f` invokes the following function, which returns `NONE` (`0`) instead
3882-
of blocking if there is no event available, and otherwise returns the event the
3883-
same way as `wait`.
3856+
Calling `$f` invokes the following function, which either returns an event that
3857+
was pending on one of the waitables in the given waitable set (the same way as
3858+
`waitable-set.wait`) or, if there is none, returns `0`.
38843859
```python
38853860
def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
38863861
trap_if(not thread.task.inst.may_leave)
3887-
trap_if(not thread.task.may_block())
38883862
wset = thread.task.inst.table.get(si)
38893863
trap_if(not isinstance(wset, WaitableSet))
3890-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
3864+
if thread.task.deliver_pending_cancel(cancellable):
3865+
event = (EventCode.TASK_CANCELLED, 0, 0)
3866+
elif not wset.has_pending_event():
3867+
event = (EventCode.NONE, 0, 0)
3868+
else:
3869+
event = wset.get_pending_event()
38913870
return unpack_event(mem, thread, ptr, event)
38923871
```
3893-
Even though `waitable-set.poll` doesn't block until the given waitable set has
3894-
a pending event, `poll_until` does transitively perform a `Thread.suspend`
3895-
which allows the embedder to nondeterministically switch to executing another
3896-
task (like `thread.yield`). To avoid encouraging spin-waiting and to support
3897-
hosts like browsers that require returning to the event loop for async I/O to
3898-
resolve, a non-`async`-typed function export that has not yet returned a value
3899-
unconditionally traps if it transitively attempts to call `poll`.
3900-
39013872
If `cancellable` is set, then `waitable-set.poll` will return whether the
39023873
supertask has already or concurrently requested cancellation.
39033874
`waitable-set.poll` (and other cancellable operations) will only indicate
@@ -3927,8 +3898,8 @@ def canon_waitable_set_drop(thread, i):
39273898
return []
39283899
```
39293900
Note that `WaitableSet.drop` will trap if it is non-empty or there is a
3930-
concurrent `waitable-set.wait` or `waitable-set.poll` or `async callback`
3931-
currently using this waitable set.
3901+
concurrent `waitable-set.wait` or `async callback` currently using this
3902+
waitable set.
39323903

39333904

39343905
### 🔀 `canon waitable.join`
@@ -4607,7 +4578,7 @@ def canon_thread_yield(cancellable, thread):
46074578
If a non-`async`-typed function export that has not yet returned a value
46084579
transitively calls `thread.yield`, it returns immediately without blocking
46094580
(instead of trapping, as with other possibly-blocking operations like
4610-
`waitable-set.poll`). This is because, unlike other built-ins, `thread.yield`
4581+
`waitable-set.wait`). This is because, unlike other built-ins, `thread.yield`
46114582
may be scattered liberally throughout code that might show up in the transitive
46124583
call tree of a synchronous function call.
46134584

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
@@ -812,7 +811,7 @@ defined by the Component Model:
812811
waitable's event is delivered first.
813812
* If multiple threads wait on or poll the same waitable set at the same time,
814813
the distribution of events to threads is nondeterministic.
815-
* Whenever a thread yields or waits on (or polls) a waitable set with an already
814+
* Whenever a thread yields or waits on a waitable set with an already
816815
pending event, whether the thread suspends and transfers execution to an
817816
async caller is nondeterministic.
818817
* If multiple threads that previously suspended can be resumed at the same
@@ -1053,8 +1052,6 @@ The `(result i32)` lets the core function return what it wants the runtime to do
10531052
to run, but resuming thereafter without waiting on anything else.
10541053
* If the low 4 bits are `2`, the callee wants to wait for an event to occur in
10551054
the waitable set whose index is stored in the high 28 bits.
1056-
* If the low 4 bits are `3`, the callee wants to poll for any events that have
1057-
occurred in the waitable set whose index is stored in the high 28 bits.
10581055

10591056
When an async stackless function is exported, a companion "callback" function
10601057
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):
@@ -2042,11 +2028,8 @@ def thread_func(thread):
20422028
wset = inst.table.get(si)
20432029
trap_if(not isinstance(wset, WaitableSet))
20442030
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2045-
case CallbackCode.POLL:
2046-
trap_if(not task.may_block())
2047-
wset = inst.table.get(si)
2048-
trap_if(not isinstance(wset, WaitableSet))
2049-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2031+
case _:
2032+
trap()
20502033
thread.in_event_loop = False
20512034
inst.exclusive = True
20522035
event_code, p1, p2 = event
@@ -2063,8 +2046,7 @@ class CallbackCode(IntEnum):
20632046
EXIT = 0
20642047
YIELD = 1
20652048
WAIT = 2
2066-
POLL = 3
2067-
MAX = 3
2049+
MAX = 2
20682050

20692051
def unpack_callback_result(packed):
20702052
code = packed & 0xf
@@ -2278,10 +2260,14 @@ def unpack_event(mem, thread, ptr, e: EventTuple):
22782260

22792261
def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
22802262
trap_if(not thread.task.inst.may_leave)
2281-
trap_if(not thread.task.may_block())
22822263
wset = thread.task.inst.table.get(si)
22832264
trap_if(not isinstance(wset, WaitableSet))
2284-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
2265+
if thread.task.deliver_pending_cancel(cancellable):
2266+
event = (EventCode.TASK_CANCELLED, 0, 0)
2267+
elif not wset.has_pending_event():
2268+
event = (EventCode.NONE, 0, 0)
2269+
else:
2270+
event = wset.get_pending_event()
22852271
return unpack_event(mem, thread, ptr, event)
22862272

22872273
### 🔀 `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
@@ -184,13 +184,20 @@
184184
(i32.const 2 (; WAIT ;))
185185
(i32.shl (call $waitable-set.new) (i32.const 4)))
186186
)
187-
(func (export "trap-if-poll")
188-
(call $waitable-set.poll (call $waitable-set.new) (i32.const 0xdeadbeef))
189-
unreachable
187+
(func (export "poll-is-fine") (result i32)
188+
(local $ret i32)
189+
(local.set $ret (call $waitable-set.poll (call $waitable-set.new) (i32.const 0)))
190+
(if (i32.ne (i32.const 0 (; NONE ;)) (local.get $ret))
191+
(then unreachable))
192+
(if (i32.ne (i32.const 0) (i32.load (i32.const 0)))
193+
(then unreachable))
194+
(if (i32.ne (i32.const 0) (i32.load (i32.const 4)))
195+
(then unreachable))
196+
(i32.const 42)
190197
)
191-
(func (export "trap-if-poll-cb") (result i32)
198+
(func (export "trap-if-invalid-callback-code") (param $invalid-code i32) (result i32)
192199
(i32.or
193-
(i32.const 3 (; POLL ;))
200+
(local.get $invalid-code)
194201
(i32.shl (call $waitable-set.new) (i32.const 4)))
195202
)
196203
(func (export "yield-is-fine") (result i32)
@@ -288,8 +295,8 @@
288295
(func (export "trap-if-suspend") (canon lift (core func $core "trap-if-suspend")))
289296
(func (export "trap-if-wait") (canon lift (core func $core "trap-if-wait")))
290297
(func (export "trap-if-wait-cb") (canon lift (core func $core "trap-if-wait-cb") async (callback (func $core "unreachable-cb"))))
291-
(func (export "trap-if-poll") (canon lift (core func $core "trap-if-poll")))
292-
(func (export "trap-if-poll-cb") (canon lift (core func $core "trap-if-poll-cb") async (callback (func $core "unreachable-cb"))))
298+
(func (export "poll-is-fine") (result u32) (canon lift (core func $core "poll-is-fine")))
299+
(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"))))
293300
(func (export "yield-is-fine") (result u32) (canon lift (core func $core "yield-is-fine")))
294301
(func (export "yield-is-fine-cb") (result u32) (canon lift (core func $core "yield-is-fine-cb") async (callback (func $core "return-42-cb"))))
295302
(func (export "trap-if-sync-call-async1") (canon lift (core func $core "trap-if-sync-call-async1")))
@@ -312,8 +319,8 @@
312319
(func (export "trap-if-suspend") (alias export $d "trap-if-suspend"))
313320
(func (export "trap-if-wait") (alias export $d "trap-if-wait"))
314321
(func (export "trap-if-wait-cb") (alias export $d "trap-if-wait-cb"))
315-
(func (export "trap-if-poll") (alias export $d "trap-if-poll"))
316-
(func (export "trap-if-poll-cb") (alias export $d "trap-if-poll-cb"))
322+
(func (export "poll-is-fine") (alias export $d "poll-is-fine"))
323+
(func (export "trap-if-invalid-callback-code") (alias export $d "trap-if-invalid-callback-code"))
317324
(func (export "yield-is-fine") (alias export $d "yield-is-fine"))
318325
(func (export "yield-is-fine-cb") (alias export $d "yield-is-fine-cb"))
319326
(func (export "trap-if-sync-cancel") (alias export $d "trap-if-sync-cancel"))
@@ -341,9 +348,13 @@
341348
(component instance $i $Tester)
342349
(assert_trap (invoke "trap-if-wait-cb") "cannot block a synchronous task before returning")
343350
(component instance $i $Tester)
344-
(assert_trap (invoke "trap-if-poll") "cannot block a synchronous task before returning")
351+
(assert_return (invoke "poll-is-fine") (u32.const 42))
352+
(component instance $i $Tester)
353+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 3)) "unsupported callback code: 3")
354+
(component instance $i $Tester)
355+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 4)) "unsupported callback code: 4")
345356
(component instance $i $Tester)
346-
(assert_trap (invoke "trap-if-poll-cb") "cannot block a synchronous task before returning")
357+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 15)) "unsupported callback code: 15")
347358
(component instance $i $Tester)
348359
(assert_return (invoke "yield-is-fine") (u32.const 42))
349360
(component instance $i $Tester)

0 commit comments

Comments
 (0)