Home

Awesome

wasi-threads

A proposed WebAssembly System Interface API to add native thread support.

NOTE: this proposal is considered a legacy proposal, retained for engines that can only support WASI v0.1 (preview1). After much debate, future work on threads will happen in the shared-everything-threads proposal which adds component model built-ins for thread spawning, among other things. The goal is that WASI v0.2 and following will use shared-everything-threads (once fully implemented) and this proposal can eventually be removed. In the meantime, users experimenting with this proposal can continue to get help with questions and bugs by opening issues on this repository and tagging various maintainers who plan to continue supporting WASI v0.1 (e.g., @loganek, @yamt, @wenyongh).

Current Phase

Phase 1

Champions

Phase 4 Advancement Criteria

TODO before entering Phase 2.

Table of Contents

Introduction

This proposal looks to provide a standard API for thread creation. This is a WASI-level proposal that augments the WebAssembly-level threads proposal. That WebAssembly-level proposal provides the primitives necessary for shared memory, atomic operations, and wait/notify. This WASI-level proposal solely provides a mechanism for spawning threads. Any other thread-like operations (thread joining, locking, etc.) will use primitives from the WebAssembly-level proposal.

Some background: browsers already have a mechanism for spawning threads — Web Workers — and the WebAssembly-level proposal avoided specifying how thread spawning should occur. This allows other uses of WebAssembly — i.e., outside the browser — to specify their own mechanism for spawning threads.

Goals

Non-goals

API walk-through

The API consists of a single function. In pseudo-code:

status wasi_thread_spawn(thread_start_arg* start_arg);

where the status is a unique non-negative integer thread ID (TID) of the new thread (see Design choice: thread IDs) or a negative number representing an error if the host failed to spawn the thread. The host implementing wasi_thread_spawn will call a predetermined function export (wasi_thread_start) in a new WebAssembly instance. Any necessary locking/signaling/thread-local storage will be implemented using existing instructions available in WebAssembly. Ideally, users will never use wasi_thread_spawn directly but rather compile their threaded code from a language that supports threads (see below).

Use case: support various languages

Using this API, it should be possible to implement threads in languages like:

The API should be able to support even more languages, but supporting these initially is a good starting point.

Use case: support thread-local storage

For languages that implement thread-local storage (TLS), the start argument can contain a language-specific structure with the address and (potentially) the length of a TLS memory region. The host WebAssembly engine will treat this argument as an opaque pointer — it should not introspect these language-specific details. In C, e.g., the start function should be a static trampoline-like wrapper (exported as wasi_thread_start) that reads the actual user start function out of the start argument and calls this after doing some TLS bookkeeping (this is not much different than how C starts threads natively).

Detailed design discussion

Threads are tricky to implement. This proposal relies on a specific convention in order to work correctly. When instantiating a module which is expected to run with wasi-threads, the WASI host must first allocate shared memories to satisfy the module's imports.

Upon a call to wasi_thread_spawn, the WASI host must:

  1. instantiate the module again — this child instance will be used for the new thread
  2. in the child instance, import all of the same WebAssembly objects, including the above mentioned shared memories, as the parent
  3. optionally, spawn a new host-level thread (other spawning mechanisms are possible)
  4. calculate a positive, non-duplicate thread ID, tid, and return it to the caller; any error in the previous steps is indicated by returning a negative error code.
  5. in the new thread, call the child instance's exported entry function with the thread ID and the start argument: wasi_thread_start(tid, start_arg)

A WASI host that implements the above should be able to spawn threads for a variety of languages.

Design choice: thread IDs

When wasi_thread_spawn successfully spawns a thread, it returns a thread ID (TID) — 32-bit integer with several restrictions. TIDs are managed and provided by the WASI host. To avoid leaking information, the host may choose to return arbitrary TIDs (as opposed to leaking OS TIDs).

Valid TIDs fall in the range $[1, 2^{29})$. Some considerations apply:

Design choice: termination

A wasi-threads module initially executes a single thread — the main thread. As wasi_thread_spawn is called, more threads begin to execute. Threads terminate in the following ways:

Design choice: pthreads

One of the goals of this API is to be able to support pthreads for C compiled to WebAssembly. Given a WASI host that implements thread_spawn as described above, what responsibility would the C language have (i.e., libc) to properly implement pthreads?

pthread_create must not only call WASI's wasi_thread_spawn but is also responsible for setting up the new thread's stack, TLS/TSD space, and updating the pthread_t structure. This could be implemented by the following steps (ignoring error conditions):

  1. configure a struct start_args with the user's void *(*start_func)(void *) and void *start_arg (as done natively) but also with pthread_t *thread
  2. call malloc (instead of mmap) to allocate TLS/TSD in the shared WebAssembly memory
  3. define a static, exported wasi_thread_start function that takes as parameters int tid and void *start_args
  4. in pthread_create, call wasi_thread_spawn with the configured start_args and use atomic.wait to wait for the start_args->thread->tid value to change (note that for web polyfills this may not be necessary since creation of web workers is not synchronous)
  5. now in the child thread: once the WASI host creates the new thread instance and calls wasi_thread_start, then a) set args->thread->tid to the host-provided tid, b) set the __wasilibc_pthread_self global to point to args->thread (this is used by pthread_self, e.g.), c) use atomic.notify to inform the parent thread that the child now has a tid, d) start executing the user's start_func with the user's start_arg — at this point the new instance is executing separately in its own thread
  6. back in the parent thread: once it has been notified that the child has recorded its TID, it can safely return with the pthread_t structure properly filled out.

pthread_join has a similar wait/notify implementation, but in reverse: the parent thread can wait on the thread->return address to change and the child thread can notify it of this once the user's start function finishes (i.e., at the end of the wasi_thread_start wrapper).

The remainder of the pthreads API can be split up into what can be implemented and what can safely be skipped until some later date.

What can easily be implemented
What can be skipped
What has been implemented

wasi-libc contains an implementation of pthreads using wasi-threads. Various WebAssembly engines support the proposal, including: Wasmtime, WAMR, Wasmer, toywasm.

Design choice: instance-per-thread

A thread spawning mechanism for WebAssembly could be implemented in various ways: the way chosen here, a cloned "instance-per-thread," is one option. The other major option is to share the instance among many threads, as described in the Weakening WebAssembly paper. Sharing an instance among many threads, as described there, would require:

The "instance-per-thread" approach was chosen here because a) it matches the thread instantiation model of the browser (also "instance-per-thread") and b) the WebAssembly specification changes required for the other approach may take some time to materialize. In the meantime, this proposal allows threaded WebAssembly to progress. If in the future the WebAssembly specification were to add a "many-threads-per-instance" mechanism, the hope is that the API here should not need to change significantly, though it is unclear how much the changes might be.

The "instance-per-thread" approach chosen here does have its disadvantages:

Considered alternatives

Alternative: WebAssembly threads

Instead of exposing threads at the WASI level, thread spawning could be specified in the WebAssembly specification. This is the approach described in the Weakening WebAssembly paper. See the Design choice: instance-per-thread discussion above for more details.

Alternative: wasi-parallel

wasi-parallel is another WASI proposal which provides a parallel "for" construct, similar to what, e.g., OpenMP provides. wasi-parallel spawns N threads at a time (though they may not all run concurrently); this API spawns a single thread at a time.

Stakeholder Interest & Feedback

TODO before entering Phase 3.

<!-- [This should include a list of implementers who have expressed interest in implementing the proposal] -->

References & acknowledgements

Many thanks for valuable feedback and advice from (alphabetical order):