WebAssembly / WebAssembly/shared-everything-threads

Making thread-local functions viable

Open
#46 17 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
WebAssembly
Stars
97
Forks
6
PR merge metrics
No merged PRs in 30d

Description

The state of things

There are two key problems that we absolutely must solve to express any reasonable compilation scheme using this proposal.

  1. How do we support thread-local storage in a compilation scheme where all Wasm functions are marked shared?
  2. How can we call JS functions from shared Wasm functions?

Discussion recap

The initial draft of this proposal aimed to solve (1) with thread-local globals, and (2) with thread-local functions. However concerns around implementation feasibility were raised about both of these (https://github.com/WebAssembly/shared-everything-threads/discussions/34, https://github.com/WebAssembly/shared-everything-threads/issues/42).

In https://github.com/WebAssembly/shared-everything-threads/issues/42 @eqrion proposed a different approach, which (interpreted minimally) involves using function parameters/contexts to pass around a thread ID (allowing (1) to be handled programmatically) and a record of important JS functions in the current thread (allowing (2) to be handled through ref.call). The latter requires us to relax our interpretation of the shared annotation to allow nonshared reference parameters.

This alternate approach has implications for future shared continuations (work stealing) which I talk about here (https://github.com/WebAssembly/shared-everything-threads/issues/44). If we believe shared continuations will exist in the future, we will need both versions of shared at once - shared-fixed (allows non-shared params) and shared-suspendable (disallows as in our current semantics). The latter will need its own mechanism for solving (2), with all of the same constraints we're trying to avoid now. Moreover, we will need to work through the design for composing shared-fixed and shared-suspendable function calls, potentially requiring extra language features to facilitate this (such as the shared-barrier discussed in the issue).

Thesis

The more I work through the details of the above, the more I'm struck by the amount of future core language complication, and standardisation effort, that we could avoid if we can find an acceptable solution for (2) now that also works with the shared-suspendable semantics.

Therefore, I believe we should redouble our efforts towards this end. If we find a solution, we avoid a lot of future mess. For now, I'm happy to consider (1) "minimally solved" through the thread ID-passing strategy.

Some possible ways forward

These are to spark discussion. I hope people can brainstorm variations of these, or other fresh ideas.

Eval in current realm

Introduce into the JS-API a new function, which I'll call eval_realm for simplicity, importable as shared. This takes a shared externref, interpreted as a string, and calls the current JS realm's eval function on this string. As discussed with @syg, shared Wasm functions will need a per-realm prototype, so I think this per-realm dispatch can be semantically justified.

Through compilation scheme wizardry (such as creating an initial table of meaningful strings), arbitrary JS access can be bootstrapped from this function, although it would be quite slow! To make key functions faster, this could be combined with the below strategy, with eval_realm used as a fall-back to invoke arbitrary JS.

Make more JS builtins shared

In the spirit of @eqrion's string-builtins, expose a larger list of functions (such as Math.*) that are importable as shared. Possibly come up with some lightweight standards process to add additional functions. This has the bonus of clearly supporting inlining optimisations, but doesn't allow the execution of arbitrary user-defined JS, unless combined with the above eval_realm strategy.

Revisit thread-local functions

I still think a weak form (ref "flavor 2" here) of thread-local function could potentially be viable in the short-term. The semantics I envisage: the thread-local function's ephemeron in a thread is guaranteed not to be collected only so long as the thread-local function is rooted through purely non-shared references in the same thread. If the above ever becomes untrue, future calls to the function in this thread may non-deterministically trap.

There are two objections:

  1. this exposes implementation-specific behaviour due to the non-deterministic trapping if the function was ever eligible to be collected;
  2. this still requires some GC engineering, to ensure that the thread-local GC knows how to follow non-shared references up to the shared thread-local function, and handle the keeping-alive of the underlying ephemeron correctly in this case. My semantics above allows some simplifying assumptions:
    1. there's no need to transitively walk the shared heap, only immediate peeks into it to find thread-local functions are needed;
    2. It's safe for the thread-local GC to walk into the thread-local function, since scary changes to the shared heap can only happen during a stop-the-world, which can't be carried out while the thread-local GC is taking place.

I believe the objections above could possibly be overcome if our alternative solutions are unattractive or require greater implementation effort, and through comparison to the implementation-specific behaviours already exposed through WeakRef and FinalizationRegistry.

If there are orthogonal concerns about the implementation complexity of thread-local function bind, it's possible to remove this function, creating a restricted form of thread-local function that can only be called in the thread in which it is first created/bound. Note the similarities to @syg's sketch for thread-bound JS objects. To facilitate "cross-thread" calls of (e.g.) console.log, the compilation scheme would need to create a thread-bound-wrapped console.log in each thread, and use the thread ID to dispatch to the correct one (so each call site for console.log in Wasm becomes a lookup in some table of thread-bound functions, based on the thread ID).

Thread-bound and weak thread-local functions still give us a forward-compatible path towards the "ideal" semantics. The former can be accomplished by re-introducing bind. The latter, through interpreting the strong semantics as turning the non-deterministic successes/failures of the weak semantics into deterministic successes.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No files, tests, or implementation entry points are named. Start by reading discussions/34 and issues/42 and /44, then compare the proposed eval_realm, shared builtins, and thread-local function approaches against the proposal's current shared-function and JS-API semantics. Done requires an agreed design rather than a localized code change.

Written by the indexing model from the issue text.

Assessment

Tech stack
wasm
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.