Multithreading - chung-leong/zigar GitHub Wiki

In order to make effective use of threads in a JavaScript-Zig hydrid application, you need basic understanding of JavaScript's execution model.

JavaScript event loop

At the heart of the language engine is the event loop. On each iteration of the loop an item is taken from the head of the event queue and processed. As a result additional items might be added to the queue. These are then in turn processed in subsequent iterations.

JavaScript is single-threaded. The event loop runs in what we refer to as the main thread. In the browser, the main thread is also responsible for the user interface.

Zig functions, as native code or WebAssembly, run in the main thread when imported into JavaScript. You can spawn addition threads using std.Thread.spawn() to avoid blocking the main thread.

When a Zig thread invokes a JavaScript function (through a function pointer), it posts a call request in the JavaScript event queue then sleeps until the event loop gets around to processing it. The thread calls std.Thread.Futex.wait() and is later awakened by a call to std.Thread.Futex.wake() in the main thread made from JavaScript. Both synchronous and asynchronous Javaqscript functions are handled in this manner. Synchronous functions awaken the waiting thread upon return while asynchronous ones do so when one of the callbacks passed to Promise.then() is called.

Multithreading in Node.js

Before any call to JavaScript, the event queue needs to be made accessible for other threads. This is accomplished by calling zigar.thread.use(). The function uses the NAPI function napi_create_threadsafe_function() to hook into the JavaScript event dispatch mechanism.

While the hook is in place the event loop will not shutdown where the event queue becomes empty, since at any time new events could be added. To allow for graceful exit the threadsafe hook must be removed. You do this by calling zigar.thread.end(). The function uses napi_release_threadsafe_function() to undo what use() has done.

Multithreading in WebAssembly

The Zig compiler currently generates code conforming to the wasi-thread specification, under which threads are Web Workers. Each worker runs the same WASM executable, with each instance attached to the same shared memory.

Shared memory is a restricted feature in the web browser. It's only available when the web page is cross-origin isolated, a level of protection activated by specific HTTP headers. This means support for multithreading is not a given. On web hosts that only offer barebone service like GitHub Pages, it's unavailable.

Shared memory also imposes a hard ceiling on memory footprint. When multithreaded is true, the WASM VM's address space is unable to grow larger than the value specified by maxMemory. The default is 64MB.

The number of active Zig-to-JavaScript-bridges is limited when multithreaded is true. For each function type you can only point to 16 different JavaScript functions.

The web browser generally doesn't allow synchronous wait in the main thread (due to the potential impact on the user interface). Functions like std.Thread.join() and std.Thread.Futex.wait() will cause a fault.


Patching WebAssembly thread implementation