@@ -388,32 +388,43 @@ learn its own index by calling the [`thread.index`] built-in.
388388
389389A suspended thread (identified by thread-table index) can be resumed at some
390390nondeterministic point in future via the [ ` thread.resume-later ` ] built-in. In
391- contrast, the [ ` thread.yield-to ` ] built-in switches execution to the given
392- thread immediately, leaving the * calling* thread to be resumed at some
393- nondeterministic point in the future. Lastly, the [ ` thread.switch-to ` ]
394- built-in switches execution to the given thread immediately, like ` yield-to ` ,
395- but leaves the calling thread in the "suspended" state. These three functions
396- can be used to resume both newly-created threads as well as threads that
397- executed and then suspended.
398-
399- In addition to threads entering the suspended state via ` thread.new-indirect `
400- and ` thread.switch-to ` , threads can also explicitly suspend themselves via the
401- [ ` thread.suspend ` ] built-in. Thus, there are three ways a thread * enters* the
402- suspended state and three ways a thread * exits* the suspended state (with
403- ` thread.switch-to ` serving in both categories). Together, these 5 thread
404- built-ins support both the "green thread" [ use cases] ( #goals ) where Core
405- WebAssembly code running inside the component wants to fully control thread
406- scheduling (via ` thread.switch-to ` and ` thread.suspend ` ) as well as the "host
407- thread" use cases where the Core WebAssembly code wants to let the containing
408- runtime nondeterministically schedule threads (via ` thread.resume-later ` or
409- ` thread.yield-to ` ).
410-
411- Lastly, since threads are cooperative, there is a [ ` thread.yield ` ] built-in
412- that can be called in the middle of long-running computations to allow the
413- runtime to nondeterministically switch execution to another thread.
414- ` thread.yield ` is equivalent to (but obviously more efficient than) creating a
415- new thread with a no-op function (via ` thread.new-indirect ` ) and then yielding
416- to it (via ` thread.yield-to ` ).
391+ contrast, the [ ` thread.yield-then-resume ` ] built-in switches execution to the
392+ given thread immediately, leaving the * calling* thread to be resumed at some
393+ nondeterministic point in the future. Lastly, the [ ` thread.suspend-then-resume ` ]
394+ built-in switches execution to the given thread immediately, like
395+ ` thread.yield-then-resume ` , but leaves the calling thread in the "suspended"
396+ state. These three functions can be used to resume both newly-created threads as
397+ well as threads that executed and then suspended.
398+
399+ Threads can also explicitly put themselves in the "suspended" state without
400+ specifying the other thread to run by calling the [ ` thread.suspend ` ] built-in.
401+ This is useful if a thread needs to wait on some condition that will be met by
402+ some unknown thread in the future (which will resume the suspended thread).
403+ Similarly, threads can explicitly put themselves in the "ready to run" state
404+ without specifying the other thread to run by calling [ ` thread.yield ` ] . This is
405+ useful if a thread has a long-running computation without I/O but still needs to
406+ allow other cooperative threads to make progress concurrently.
407+
408+ Lastly, in addition to being able to switch to "suspended" threads, threads can
409+ also switch to threads that are in a "ready to run" state by calling the
410+ [ ` thread.suspend-then-promote ` ] and [ ` thread.yield-then-promote ` ] built-ins
411+ which, like the ` thread.{suspend,yield}-then-resume ` built-ins, leave the
412+ calling thread in a "suspended" or "ready to run" state, resp. While the calling
413+ thread * may* know that the target thread is ready to run (e.g., because the
414+ target thread yielded or is waiting on a future/stream operation that the
415+ calling thread just completed), in general, readiness may depend on
416+ nondeterministic external I/O and the calling thread just wants to yield its
417+ timeslice to the target thread * if* it's ready. Thus, if the target thread
418+ is * not* "ready to run", these built-ins gracefully fall back to the behavior of
419+ the ` thread.{suspend,yield} ` built-ins.
420+
421+ Together, these thread built-ins support both the "green thread" [ use
422+ cases] ( #goals ) , where Core WebAssembly code running inside the component wants
423+ to fully control thread scheduling (via the suspending and resuming built-ins),
424+ and the "host thread" use cases, where the Core WebAssembly code wants to let
425+ the containing runtime nondeterministically schedule threads (via the yielding
426+ and promoting built-ins).
427+
417428
418429### Thread-Local Storage
419430
@@ -467,47 +478,73 @@ For more information, see [`context.get`] in the AST explainer.
467478### Blocking
468479
469480When a thread calls an import using the async ABI, the Component Model
470- guarantees that if the callee ** blocks** , control flow is immediately returned
471- back to the caller. When the callee is implemented by the * host * , what counts as
472- "blocking" is up to the host; e.g., the host can arbitrarily determine whether
473- file I/O "blocks" or not depending on whether the host is implemented using
474- traditional synchronous OS syscalls or an asynchronous ` io_uring ` . However,
475- when the callee is implemented by another component, the Component Model
476- defines exactly what counts as "blocking".
477-
478- At a high level, there are six ways for a call to a component export to block,
479- all of which are described above or below in more detail:
480- * calling an ` async ` -typed function import using the sync ABI
481+ guarantees that if the callee [ task ] ( #threads-and-tasks ) ** blocks** , control
482+ flow is immediately returned back to the caller's thread . When the callee is
483+ implemented by the * host * , what counts as "blocking" is up to the host; e.g.,
484+ the host can arbitrarily determine whether file I/O "blocks" or not depending on
485+ whether the host is implemented using traditional synchronous OS syscalls or an
486+ asynchronous ` io_uring ` . However, when the callee is implemented by another
487+ component, the Component Model defines what counts as "blocking".
488+
489+ There are several ways for a task to potentially " block":
490+ * synchronously calling an ` async ` function that transitively blocks
491+ or hits [ backpressure ] ( #backpressure )
481492* suspending the current thread via the
482- [ ` thread.suspend ` ] ( #thread-built-ins ) built-in
493+ [ ` thread.suspend{,-then-promote} ` ] ( #thread-built-ins ) built-ins
483494* cooperatively yielding (e.g., during a long-running computation) via the
484- [ ` thread.yield ` ] ( #thread-built-ins ) built-in
495+ [ ` thread.yield{,-then-promote} ` ] ( #thread-built-ins ) built-ins or, when
496+ using the stackless ` callback ` ABI, returning with the ` YIELD ` code
485497* waiting for one of a set of concurrent operations to complete via the
486- [ ` waitable-set.wait ` ] ( #waitables-and-waitable-sets ) built-in
487- * waiting for a stream or future operation to complete via the
498+ [ ` waitable-set.wait ` ] ( #waitables-and-waitable-sets ) built-in or, when
499+ using the stackless ` callback ` ABI, returning with the ` WAIT ` code
500+ * synchronously waiting for a stream or future operation to complete via the
488501 [ ` {stream,future}.{,cancel-}{read,write} ` ] ( #streams-and-futures ) built-ins
489- * waiting for a subtask to cooperatively cancel itself via the
502+ * synchronously waiting for a subtask to cooperatively cancel itself via the
490503 [ ` subtask.cancel ` ] ( #cancellation ) built-in
491504
492- At each of these points, the [ current thread] ( #current-thread-and-task ) will be
493- suspended. Execution transfers to a caller's thread if there is one, or
494- otherwise back to the runtime, which may invoke new component exports or
495- nondeterministically resume a cooperative thread that is ready to run. Thus,
496- each of these represents ** cooperative yield points** .
497-
498- Additionally, each of these potentially-blocking operations will trap if the
499- [ current task's function type] ( #current-thread-and-task ) does not declare the
500- ` async ` effect, since only ` async ` -typed functions are allowed to block. As an
501- exception, to allow it to be called arbitrarily from anywhere, ` thread.yield `
502- does not trap but instead behaves as a no-op if the current task's function
503- type does not contain ` async ` .
505+ Since Component Model concurrency is [ specified in terms of] [ stack-switching] ,
506+ each of the above represents a point where the [ current thread] ( #current-thread-and-task )
507+ may suspend with the ` $block ` effect. Each of these also serves as a
508+ ** cooperative yield point** where [ Component Invariant] #2 allows reentrance.
509+ However, just because the current thread * suspends* doesn't mean that the * task*
510+ has officially "blocked": what happens next depends on the state of the task and
511+ the declared function type:
512+
513+ If the task has already [ returned] ( #returning ) a value to the caller, then
514+ control flow returns to the caller and, from the caller's perspective, the call
515+ returns normally without blocking. The callee's threads can continue executing,
516+ but whether or not these threads suspend doesn't matter to the caller anymore.
517+
518+ If instead the task has * not* yet returned a value and the callee's function
519+ type declares the ` async ` effect, control flow transfers immediately to the
520+ caller when the callee suspends. If the caller used the * async* ABI, then
521+ control flow returns to Core WebAssembly, indicating that the call "blocked" by
522+ returning the non-zero index of a new [ subtask] ( #subtasks-and-supertasks ) . If
523+ the caller used the * sync* ABI, then the caller itself suspends with the
524+ ` $block ` effect and this process repeats recursively up the stack.
525+
526+ Lastly, if the task has not yet returned a value and the callee's function type
527+ does * not* declare the ` async ` effect, then the task is not allowed to "block"
528+ and will trap if it ends up blocking. However, just because the * current thread*
529+ has suspended doesn't mean that the overall * task* is blocked: if there are any
530+ other threads in the callee's component instance that are in the [ "ready to
531+ run"] ( #thread-built-ins ) state, they may lead to returning a value (e.g., by
532+ releasing a lock or computing a dependency or returning the value themselves).
533+ Thus, the Component Model repeatedly resumes threads that are ready to run (only
534+ in the callee's component instance; picking nondeterministically if there are
535+ multiple) until either the task returns a value (in which case control flow
536+ returns to the caller who sees that the call did not block) or there are no more
537+ ready threads in the callee's component instance (in which case the call traps).
538+
539+ These rules achieve expressive parity with what would otherwise be possible
540+ using a CPS transform like [ Asyncify] to implement a synchronous function. For
541+ example, they allow synchronous functions to be implemented by pthreads that
542+ switch at cooperative yield points. And if there is only a single pthread, since
543+ ` thread.yield ` always leaves the calling thread in a "ready to run" state,
544+ ` thread.yield ` effectively becomes a no-op (until a value is returned). The
545+ limitation of only switching to cooperative threads * in the same component
546+ instance* is necessary to preserve [ Component Invariant] #2 .
504547
505- "Blocking" is [ specified in terms of] stack-switching, with a ` block ` effect
506- that suspends the current thread to produce a continuation that can be resumed
507- once the reason for blocking is addressed.
508-
509- The [ Canonical ABI explainer] defines the above behavior more precisely; search
510- for ` may_block ` to see all the relevant points.
511548
512549### Waitables and Waitable Sets
513550
@@ -678,12 +715,12 @@ degree of concurrency than synchronous exports. Stackful async exports ignore
678715the lock entirely and thus achieve the highest degree of (cooperative)
679716concurrency.
680717
681- Since non-` async ` functions are not allowed to block (including due to
682- backpressure) and also don't pile up like ` async ` functions, non-` async `
683- functions ignore backpressure (explicit and implicit) entirely. If a
684- component exports a mix of ` async ` and non-` async ` functions, code generation
685- must therefore be prepared to handle non-` async ` functions executing at
686- any cooperative yield point, even in the middle of a ` callback ` .
718+ Since non-` async ` functions are not allowed to [ block] ( #blocking ) (including due
719+ to backpressure) and also don't pile up like ` async ` functions, non-` async `
720+ functions ignore backpressure (explicit and implicit) entirely. If a component
721+ exports a mix of ` async ` and non-` async ` functions, code generation must
722+ therefore be prepared to handle non-` async ` functions executing at any
723+ cooperative yield point, even in the middle of a ` callback ` .
687724
688725Once a task is allowed to start according to these backpressure rules, its
689726arguments are lowered into the callee's linear memory and the task is in
@@ -713,9 +750,7 @@ the readable end passed for `in`) and `stream.write`s (of the writable end it
713750` stream.new ` ed) before exiting the task.
714751
715752Once ` task.return ` is called, the task is in the "returned" state. Calling
716- ` task.return ` when not in the "started" state traps. Once in a "returned" state,
717- non-` async ` functions may block using cooperative threads that were created
718- before the synchronous task's implicit thread returned.
753+ ` task.return ` when not in the "started" state traps.
719754
720755### Borrows
721756
@@ -1538,13 +1573,15 @@ comes after:
15381573[ `waitable-set.wait` ] : Explainer.md#-waitable-setwait
15391574[ `waitable-set.poll` ] : Explainer.md#-waitable-setpoll
15401575[ `waitable.join` ] : Explainer.md#-waitablejoin
1541- [ `thread.new-indirect` ] : Explainer.md#-threadnew-indirect
15421576[ `thread.index` ] : Explainer.md#-threadindex
1543- [ `thread.suspend` ] : Explainer.md#-threadsuspend
1544- [ `thread.switch-to` ] : Explainer.md#-threadswitch-to
1577+ [ `thread.new-indirect` ] : Explainer.md#-threadnew-indirect
15451578[ `thread.resume-later` ] : Explainer.md#-threadresume-later
1546- [ `thread.yield-to ` ] : Explainer.md#-threadyield-to
1579+ [ `thread.suspend ` ] : Explainer.md#-threadsuspend
15471580[ `thread.yield` ] : Explainer.md#-threadyield
1581+ [ `thread.suspend-then-resume` ] : Explainer.md#-threadsuspend-then-resume
1582+ [ `thread.yield-then-resume` ] : Explainer.md#-threadyield-then-resume
1583+ [ `thread.suspend-then-promote` ] : Explainer.md#-threadsuspend-then-promote
1584+ [ `thread.yield-then-promote` ] : Explainer.md#-threadyield-then-promote
15481585[ `{stream,future}.new` ] : Explainer.md#-streamnew-and-futurenew
15491586[ `{stream,future}.{read,write}` ] : Explainer.md#-streamread-and-streamwrite
15501587[ `stream.cancel-write` ] : Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write
0 commit comments