@@ -388,32 +388,44 @@ 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. The calling
413+ thread * may* know that the target thread is ready to run (e.g., because the
414+ target thread is known to have yielded or to be waiting on a future/stream
415+ operation that the calling thread just completed). However, in general,
416+ readiness may depend on nondeterministic external I/O and the calling thread may
417+ just want to yield its timeslice to the target thread * if* it's ready as a
418+ scheduling optimization. Thus, if the target thread is * not* "ready to run",
419+ these built-ins are defined to gracefully fall back to the behavior of the
420+ ` thread.{suspend,yield} ` built-ins.
421+
422+ Together, these thread built-ins support both the "green thread" [ use
423+ cases] ( #goals ) , where Core WebAssembly code running inside the component wants
424+ to fully control thread scheduling (via suspending and resuming built-ins),
425+ and the "host thread" use cases, where the Core WebAssembly code wants to let
426+ the containing runtime nondeterministically schedule threads (via yielding
427+ built-ins) with hints (via the promoting built-ins) — or a mixture of both.
428+
417429
418430### Thread-Local Storage
419431
@@ -467,47 +479,70 @@ For more information, see [`context.get`] in the AST explainer.
467479### Blocking
468480
469481When 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
482+ guarantees that if the callee [ task ] ( #threads-and-tasks ) ** blocks** , control
483+ flow is immediately returned back to the caller's thread . When the callee is
484+ implemented by the * host * , what counts as "blocking" is up to the host; e.g.,
485+ the host can arbitrarily determine whether file I/O "blocks" or not depending on
486+ whether the host is implemented using traditional synchronous OS syscalls or an
487+ asynchronous ` io_uring ` . However, when the callee is implemented by another
488+ component, the Component Model defines what counts as "blocking".
489+
490+ There are several ways for a task to potentially " block":
491+ * synchronously calling an ` async ` function that transitively blocks
492+ or hits [ backpressure ] ( #backpressure )
481493* suspending the current thread via the
482- [ ` thread.suspend ` ] ( #thread-built-ins ) built-in
494+ [ ` thread.suspend{,-then-promote} ` ] ( #thread-built-ins ) built-ins
483495* cooperatively yielding (e.g., during a long-running computation) via the
484- [ ` thread.yield ` ] ( #thread-built-ins ) built-in
496+ [ ` thread.yield{,-then-promote} ` ] ( #thread-built-ins ) built-ins or, when
497+ using the stackless ` callback ` ABI, returning with the ` YIELD ` code
485498* 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
499+ [ ` waitable-set.wait ` ] ( #waitables-and-waitable-sets ) built-in or, when
500+ using the stackless ` callback ` ABI, returning with the ` WAIT ` code
501+ * synchronously waiting for a stream or future operation to complete via the
488502 [ ` {stream,future}.{,cancel-}{read,write} ` ] ( #streams-and-futures ) built-ins
489- * waiting for a subtask to cooperatively cancel itself via the
503+ * synchronously waiting for a subtask to cooperatively cancel itself via the
490504 [ ` subtask.cancel ` ] ( #cancellation ) built-in
491505
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 ` .
506+ Since Component Model concurrency is [ specified in terms of] the Core WebAssembly
507+ [ stack-switching] proposal, each of the above represents a point where the
508+ [ current thread] ( #current-thread-and-task ) may suspend with the ` $block ` effect.
509+ Each of these points also serves as a ** cooperative yield point** where
510+ [ Component Invariant] #2 allows reentrance. However, just because the current
511+ thread * suspends* doesn't mean that the * task* has officially "blocked": what
512+ happens next depends on the state of the task and the declared function type:
513+
514+ If the task has already [ returned] ( #returning ) a value to the caller, then
515+ control flow returns to the caller and, from the caller's perspective, the call
516+ returns normally without blocking. The callee's threads can continue executing,
517+ but what happens with these threads no longer matters to the caller.
518+
519+ If instead the task has * not* yet returned a value and the callee's function
520+ type declares the ` async ` effect, control flow returns directly to the
521+ caller. If the caller used the * async* ABI, then control flow returns to Core
522+ WebAssembly, indicating that the call "blocked" by returning the non-zero index
523+ of a new [ subtask] ( #subtasks-and-supertasks ) . If the caller used the * sync* ABI,
524+ then the caller immediately suspends with the ` $block ` effect and this process
525+ repeats recursively up the stack.
526+
527+ Lastly, if the task has not yet returned a value and the callee's function type
528+ does * not* declare the ` async ` effect, then the task is not allowed to "block"
529+ and will trap if it ends up blocking. However, just because the * current thread*
530+ has suspended doesn't mean that the overall * task* is blocked: if there are any
531+ other threads in the callee's component instance that are in the [ "ready to
532+ run"] ( #thread-built-ins ) state, progressing them may unblock returning a value
533+ (e.g., by releasing a lock or computing a dependency) and so the Component Model
534+ repeatedly resumes threads that are ready to run until either the task returns a
535+ value or there are no more eligible ready threads (and the call traps).
536+ See the end of [ ` canon_lift ` ] in the Canonical ABI Explainer for more details.
537+
538+ These rules achieve expressive parity with what would otherwise be possible
539+ using a CPS transform like [ Asyncify] to implement a synchronous function. For
540+ example, they allow synchronous functions to be implemented by pthreads that
541+ switch and make progress at cooperative yield points. And if there is only a
542+ single pthread, since ` thread.yield ` always leaves the calling thread in a
543+ "ready to run" state, ` thread.yield ` effectively becomes a no-op (until a value
544+ is returned).
504545
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.
511546
512547### Waitables and Waitable Sets
513548
@@ -713,9 +748,7 @@ the readable end passed for `in`) and `stream.write`s (of the writable end it
713748` stream.new ` ed) before exiting the task.
714749
715750Once ` 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.
751+ ` task.return ` when not in the "started" state traps.
719752
720753### Borrows
721754
@@ -1538,13 +1571,15 @@ comes after:
15381571[ `waitable-set.wait` ] : Explainer.md#-waitable-setwait
15391572[ `waitable-set.poll` ] : Explainer.md#-waitable-setpoll
15401573[ `waitable.join` ] : Explainer.md#-waitablejoin
1541- [ `thread.new-indirect` ] : Explainer.md#-threadnew-indirect
15421574[ `thread.index` ] : Explainer.md#-threadindex
1543- [ `thread.suspend` ] : Explainer.md#-threadsuspend
1544- [ `thread.switch-to` ] : Explainer.md#-threadswitch-to
1575+ [ `thread.new-indirect` ] : Explainer.md#-threadnew-indirect
15451576[ `thread.resume-later` ] : Explainer.md#-threadresume-later
1546- [ `thread.yield-to ` ] : Explainer.md#-threadyield-to
1577+ [ `thread.suspend ` ] : Explainer.md#-threadsuspend
15471578[ `thread.yield` ] : Explainer.md#-threadyield
1579+ [ `thread.suspend-then-resume` ] : Explainer.md#-threadsuspend-then-resume
1580+ [ `thread.yield-then-resume` ] : Explainer.md#-threadyield-then-resume
1581+ [ `thread.suspend-then-promote` ] : Explainer.md#-threadsuspend-then-promote
1582+ [ `thread.yield-then-promote` ] : Explainer.md#-threadyield-then-promote
15481583[ `{stream,future}.new` ] : Explainer.md#-streamnew-and-futurenew
15491584[ `{stream,future}.{read,write}` ] : Explainer.md#-streamread-and-streamwrite
15501585[ `stream.cancel-write` ] : Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write
0 commit comments