Skip to content

Conversation

@lukewagner
Copy link
Member

This PR extends the concurrency support in Preview 3 with the ability for core wasm to create and switch between "cooperative threads", providing fiber-like functionality that is exposed to core wasm as plain core function imports that are integrated with and built on the async machinery that is already part of Preview 3.

Cooperative threads are meant to be released in some 0.3.x minor version after 0.3.0, so they are given a separate emoji-gate than 0.3.0's 🔀. Since cooperative threads pave the way for preemptive threads (which depend on shared-everything-threads), the emoji-gate for cooperative threads is 🧵 and preemptive threads are switched to 🧵②. The current emoji-gates 🚝 and 🚟 are folded into 🧵 since they're also post-0.3.0 and make sense only with cooperative threads but if there's still a reason to keep them separate, we can split them back out.

This feature is intended to cover both "host threads" (e.g., pthreads.h) and "green threads" (e.g. goroutines and Java virtual threads) use cases. The former use case mostly only needs the ability to spawn a thread (with, in a cooperative setting, the choice between whether to run the new thread immediately or "later"). But the green-threads use case needs to take explicit control of switching between threads (instead of leaving it up to the wasm runtime), so several additional built-ins are added to do this such as thread.switch-to (which works like the switch instruction in the stack-switching proposal). For the summary of the built-ins, see this section. Also see the rewritten goals and summary in Concurrency.md (formerly Async.md).

With the above new built-ins, thread.spawn_indirect becomes an optimized fusion of thread.new_indirect+thread.resume-later. For simplicity, thread.spawn_indirect is kept gated to 🧵②, so that it's "the one that depends on shared" and thread.new_indirect is added by 🧵 and has no shared option for now. As noted in the Binary format TODO, as part of 1.0-rc, we'll need to add a shared? option to every existing built-in (so that they can all be called from preemptive threads) anyhow. But this PR does change the binary format for thread.spawn_indirect (to include an optional shared immediate) so CC @abrown on these changes in Binary.md. (It's fine to not implement them any time soon, but I think it's useful to see the sketch of how they'd eventually look.)

For consistency, the yield built-in is backwards-compatibly renamed thread.yield (keeping the opcode the same and keeping yield in the text format but marked "deprecated" until the transition is done).

What was previously called "context-local storage" in the explainer is now renamed to "thread-local storage" (since it's now literally stored per thread), but the context.{get,set} built-ins keep the same name since they still seem to make sense as names. As planned earlier, the static length of the thread-local storage array is bumped from "1" to "2" b/c now there's a good reason to store the "linear memory shadow stack pointer" alongside the "general TLS struct pointer".

Although a bunch of the explanatory prose changes in this PR, the actual functional addition to what was already necessary for 0.3.0 is surprisingly little; there's mostly just this new ability to directly thread.switch-to. Hopefully that remains true in the implementation.

I expect to keep this PR open for a while until someone with an implementor's hat on has a chance to look carefully, so no rush for folks to review. I'd suggest reading the diff of Concurrency.md first then Explainer.md then CanonicalABI.md.

@lukewagner lukewagner force-pushed the threads branch 3 times, most recently from 3c057e8 to 0c0e6ea Compare September 8, 2025 21:17
@lukewagner
Copy link
Member Author

I realized an interesting corner case (introduced by #548 removing the "suspend-the-whole-instance" behavior of sync imports) that isn't observable in 0.3.0 that becomes observable with either cooperative threads or stackful async: if one thread blocks on a synchronous {stream,future}.{read,write}, another thread in the same component instance can resume and attempt to {stream,future}.cancel-{read,write} it. I believe the desired behavior is to trap (since the sync read/write isn't expecting cancellation) like we do with other sync non-cancellable built-ins, and so that's what this commit adds.

@TartanLlama
Copy link

Merged into wasm-tools: bytecodealliance/wasm-tools#2298

@TartanLlama
Copy link

Implemented in Wasmtime here: bytecodealliance/wasmtime#11751
I think this is good to merge!

import calls in a way that complements, but doesn't depend on, new Core
WebAssembly proposals including [stack-switching] and
[shared-everything-threads].
* Allow polyfilling in browsers via JavaScript Promise Integration ([JSPI])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have a complete set of cooperative threading intrinsics, it would be great to expand on this, possibly in its own document. In particular, I'd like to see a rough sketch of how the various thread.* intrinsics might be implemented in terms of JSPI. It wouldn't have to go into a lot of detail -- just enough to demonstrate that the intrinsics can be implemented in the browsers and standalone JS engines we have today (rather than needing to wait for core stack switching).

No need to include that in this PR, but would be worth a follow-up PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, great point; will do.

Copy link
Collaborator

@dicej dicej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; thanks for doing this!

| 0x41 ft:<typeidx> tbl:<core:tableidx> => (canon thread.spawn_indirect ft tbl (core func)) 🧵
| 0x42 => (canon thread.available_parallelism (core func)) 🧵
| 0x26 => (canon thread.index (core func)) 🧵
| 0x27 ft:<typeidx> tbl:<core:tableidx> => (canon thread.new_indirect ft tbl (core func)) 🧵
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: What's the reason for sometimes using underscores and sometimes using dashes in these names? (I realize we were already doing that before this PR, but I only now noticed it.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I just noticed you switched thread.available_parallelism to thread.available-parallelism in this PR; maybe we should do the same for the others that still use underscores?

`call_indirect`). Lastly, the indexed function is called in the new thread
with `c` as its first and only parameter.

Currently, `FuncT` must be `(func (param i32))` and thus `c` must always an
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Currently, `FuncT` must be `(func (param i32))` and thus `c` must always an
Currently, `FuncT` must be `(func (param i32))` and thus `c` must always be an

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants