@@ -268,11 +268,16 @@ Thread
268268where a ** component store** is the top-level "thing" and analogous to a Core
269269WebAssembly [ store] .
270270
271- The reason for the thread/task split is that, when one thread creates a new
272- thread, the new thread is contained by the task of the original thread which
273- creates an N:1 relationship between threads and tasks that ties N threads to
274- the original export call (= "task") that transitively spawned those N threads.
275- This relationship serves several purposes described in the following sections.
271+ When a component export is called, one new task is created for the call; this
272+ task contains one new * implicit* thread that executes it. The reason for the
273+ thread/task split is that this implicit thread may then go on to spawn N more
274+ * explicit* threads (via ` thread.new-indirect ` ) that are initially contained by
275+ the same task, thereby creating an N:1 relationship between threads and tasks.
276+
277+ While the store: instance and instance: task relationships are immutably set on
278+ creation, the task: thread relationship is * mutable* : guest code running inside a
279+ component instance can change which task a thread is currently executing on
280+ behalf of by calling the [ ` thread.set-task ` ] built-in, as described below.
276281
277282In the Canonical ABI explainer, threads, tasks, component instances and
278283component stores are represented by the [ ` Thread ` ] , [ ` Task ` ] ,
@@ -311,9 +316,15 @@ supertask, they can be thought of as a single node in the async call stack.
311316
312317A subtask/supertask relationship is immutably established when an import is
313318called, setting the [ current task] ( #current-thread-and-task ) as the supertask
314- of the new subtask created for the import call. Thus, one reason for
315- associating every thread with a "containing task" is to ensure that there is
316- always a well-defined async call stack.
319+ of the new subtask created for the import call. Thus, one reason for associating
320+ every thread with a "containing task" is to ensure that there is always a
321+ well-defined async call stack. Note that guest code can call [ ` thread.set-task ` ]
322+ to change the containing task of a thread and thus the async call stack can
323+ change completely between two program points while executing a single thread.
324+ For example, when a JS runtime flushes its microtask queue and encounters a JS
325+ callback associated with a task other than the current thread's task, the JS
326+ runtime would call ` thread.set-task ` so that the async call stack matches the JS
327+ developer's expectation.
317328
318329The async call stack is not currently observable to running components, except
319330that it may nondeterministically appear as part of the callstack stored in
@@ -336,9 +347,9 @@ not enforcing a stricter form of Structured Concurrency at the Component Model
336347level is that there are important use cases where forcing a supertask's thread
337348to stay resident just to wait for subtasks to finish would waste resources
338349without tangible benefit. Instead, we can say that once a supertask's last
339- thread finishes execution , the supertask semantically "tail calls" any still-
340- executing subtasks, staying technically-alive and on the async call stack until
341- they complete, but not consuming real resources.
350+ thread exits or switches to another task , the supertask semantically "tail
351+ calls" any still- executing subtasks, staying technically-alive and on the
352+ async call stack until they complete, but not consuming real resources.
342353
343354For scenarios where one component wants to * non-cooperatively* put an upper
344355bound on execution of a call into another component, a separate "[ blast zone] "
@@ -374,13 +385,23 @@ New threads are created with the [`thread.new-indirect`] built-in. As mentioned
374385[ above] ( #threads-and-tasks ) , a spawned thread inherits the task of the spawning
375386thread which is why threads and tasks are N:1. ` thread.new-indirect ` adds a new
376387thread to the component instance's threads table and returns the ` i32 ` index of
377- this table entry to the Core WebAssembly caller. Like [ ` pthread_create ` ] ,
378- ` thread.new-indirect ` takes a Core WebAssembly function (via index into a
379- ` funcref ` table) and a "closure" parameter to pass to the function when called
380- on the new thread. However, unlike ` pthread_create ` , the new thread is
381- initially in a "suspended" state and must be explicitly "resumed" using one of
382- the following 3 thread built-ins. Once the thread is resumed, the thread can
383- learn its own index by calling the [ ` thread.index ` ] built-in.
388+ this table entry to the Core WebAssembly caller.
389+
390+ After creation, the implicitly-set containing task of a thread can be explicitly
391+ overridden using the [ ` thread.set-task ` ] built-in. ` thread.set-task ` sets the
392+ containing task of the current thread to a task handle that was retrieved via
393+ [ ` thread.get-task ` ] . ` thread.get-task ` always allocates a fresh handle storing a
394+ reference to the current thread's containing task. The ` i32 ` index of this new
395+ handle must later be explicitly dropped via [ ` task.drop ` ] to avoid leaking
396+ the task. Tasks are thus kept alive by any or all of: contained threads, subtask
397+ handles and task handles.
398+
399+ Like [ ` pthread_create ` ] , ` thread.new-indirect ` takes a Core WebAssembly function
400+ (via index into a ` funcref ` table) and a "closure" parameter to pass to the
401+ function when called on the new thread. However, unlike ` pthread_create ` , the
402+ new thread is initially in a "suspended" state and must be explicitly "resumed"
403+ using one of the following 3 thread built-ins. Once the thread is resumed, the
404+ thread can learn its own index by calling the [ ` thread.index ` ] built-in.
384405
385406A suspended thread (identified by thread-table index) can be resumed at some
386407nondeterministic point in future via the [ ` thread.resume-later ` ] built-in. In
@@ -733,10 +754,7 @@ the "started" state.
733754The way an ` async ` export returns its value using the async ABI is by calling
734755[ ` task.return ` ] , passing the core values that are to be lifted as * parameters* .
735756When using the async ABI, * any* of the threads contained by a task can call
736- ` task.return ` ; there is no "main thread" of a task. When the last thread of a
737- task returns, there is a trap if ` task.return ` has not been called. Thus, * some*
738- thread (either the thread created implicitly for the initial export call or some
739- thread transitively created by that thread) must call ` task.return ` .
757+ ` task.return ` ; there is no "main thread" of a task.
740758
741759Returning values by calling ` task.return ` allows a task to continue executing
742760even after it has passed its initial results to the caller. This is also
@@ -752,7 +770,7 @@ the readable end passed for `in`) and `stream.write`s (of the writable end it
752770` stream.new ` ed) before exiting the task.
753771
754772Once ` task.return ` is called, the task is in the "returned" state. Calling
755- ` task.return ` when not in the "started" state traps.
773+ ` task.return ` when already in a resolved state traps.
756774
757775### Borrows
758776
@@ -1564,6 +1582,9 @@ the concurrency story:
15641582[ `thread.yield-then-resume` ] : Explainer.md#-threadyield-then-resume
15651583[ `thread.suspend-then-promote` ] : Explainer.md#-threadsuspend-then-promote
15661584[ `thread.yield-then-promote` ] : Explainer.md#-threadyield-then-promote
1585+ [ `thread.set-task` ] : Explainer.md#-threadset-task
1586+ [ `thread.get-task` ] : Explainer.md#-threadget-task
1587+ [ `task.drop` ] : Explainer.md#-taskdrop
15671588[ `{stream,future}.new` ] : Explainer.md#-streamnew-and-futurenew
15681589[ `{stream,future}.{read,write}` ] : Explainer.md#-streamread-and-streamwrite
15691590[ `stream.cancel-write` ] : Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write
0 commit comments