Shared-Memory Threads #
Status: active Last reviewed: 2026-10-19 Owner: theMackabu
Goal #
Give Ant programs shared-memory parallelism with a new Thread(fn) API in
the spirit of WebKit's "Concurrent JavaScript: It can work!" (2017), built on
the isolate-per-thread runtime instead of one shared heap. Programs share
fixed-layout objects, arrays, mutexes and conditions by reference across
threads and workers. Code that never touches shared objects must not get
slower: no new checks on ordinary property, call or allocation fast paths.
Background and Direction #
The WebKit design shares the whole heap: any object can be reshaped from any
thread. That needs transition-thread-locality tags in pointer bits, segmented
butterflies, buffered inline-cache updates and a multi-mutator GC; WebKit
estimated ~7x for transitions on shared objects and ~2x for their other
accesses, and never shipped it (bug 174276). In Ant it would reverse the
isolate ownership work in worker-threads-isolates.md:
ant_t *jsappears ~5,300 times in 212 files and mixes per-thread state (vm,this_val,current_func, exception state) with heap state (object list, arenas, shapes, intern table, GC).ant_object_tholdsshapeandoverflow_propas separate words, so a transition is two non-atomic stores; arrays reallocateu.array.data; every allocation links into the isolate'sobjectslist.- The GC is stop-the-world with one mutator; ICs, feedback sidecars, MIR code, rope flattening and interning all mutate without synchronization.
Fixed-layout shared objects remove the hardest part (concurrent reshaping). Ant is well placed for them because every isolate lives in one pointer cage: a NaN-boxed reference to a shared region means the same thing in every isolate, with no handle translation.
The reference semantics are V8's experimental shared structs (Node 22,
node --harmony-struct), checked on 2026-10-19:
new SharedStructType(['x', 'y'])returns a constructor; instances have anullprototype, are sealed and non-extensible; adding a field throwsTypeError: Cannot add property z, object has fixed layout.- Fields accept
undefined,null, booleans, numbers, strings and shared objects; BigInt, Symbol, ordinary objects, arrays, functions and evenSharedArrayBufferthrowTypeError: ... cannot be shared. new SharedArray(n)is fixed length, filled withundefined; out-of-range stores throw the fixed-layout error.Atomics.Mutex.lock(m, fn)returnsfn(); alsotryLock,lockWithTimeout,isMutex.Atomics.Condition.wait(c, m[, timeout]),notify(c[, count]),isCondition.- Shared objects keep identity through
workerDataandpostMessage.
API #
import { Thread, Lock, Condition, ThreadLocal } from 'ant:thread';
const Counter = new SharedStructType(['value', 'lock']);
const counter = new Counter();
counter.value = 0;
counter.lock = new Lock();
const workers = Array.from({ length: 4 }, () =>
new Thread((c, n) => {
for (let i = 0; i < n; i++) c.lock.hold(() => { c.value++; });
return c.value;
}, counter, 1e5));
await Promise.all(workers.map(t => t.asyncJoin()));
new Thread(fn, ...args)runsfn(...args)on a new isolate thread.fnmust be closure-free (no captured upvalues, not bound, not native); the constructor throwsTypeErrorotherwise. Its source is re-evaluated in the child, like a workerevalentry. Shared objects inargspass by reference; everything else is structured-cloned.thread.join()blocks and returns the result or rethrows; allowed on any thread, matchingAtomics.waiton Node's main thread.thread.asyncJoin()returns a promise.Thread.currentis the calling thread'sThread(the main thread has one too).LockandConditionare WebKit's names overAtomics.MutexandAtomics.Condition:lock.hold(fn),lock.asyncHold(),cond.wait(lock),cond.asyncWait(lock),cond.notify(),cond.notifyAll().ThreadLocalexposesvalueper isolate. In the isolate model every global is already thread-local; it exists for API parity.Thread.restrictfrom the WebKit design is not needed: ordinary objects can never cross threads.
Phases #
Each phase lands separately with tests, TSan on the thread tests and the GC gauntlet (fixture, apps2, v8fixed) showing no instruction-count regression.
1. Shared synchronization objects #
Atomics.MutexandAtomics.Conditionas refcounted native objects living outside any isolate, wire-serialized by reference likeSharedArrayBufferblocks, so they work withWorker,worker_threadsand laterThread.- Blocking waits reuse the futex-style wait queue in
src/modules/atomics.c; async waits reuse thewaitAsyncpath (uv_async_tper waiter). Waiting stays interruptible byterminate()throughatomics_wake_isolate. Lock/Conditionwrappers and theant:threadmodule skeleton.- Acceptance: Node
--harmony-structparity forlock,tryLock,lockWithTimeout,wait,notifyacross workers; a contention test.
2. new Thread(fn) on isolates #
- A third front end in
src/modules/worker/(thread.c) onworker_ops_t: entry is the function source plus a wire of the arguments; the result or error comes back through the existing event queue. - Closure-free check:
sv_closure_twith no upvalues, nobound_argc, not a cfunc; class constructors and generators rejected for now. - Cost note: each
Threadcreates an isolate (milliseconds). A follow-up can pool idle isolates; measure before deciding. - Acceptance: join/asyncJoin/error propagation/terminate tests; TSan.
3. Shared arena and SharedStructType #
- A process-wide fixed arena in the cage for shared objects, with its own
arena id in the owner table so
fixed_arena_holds_offsetkeeps isolates' marking from treating shared offsets as their own. Allocation is a lock-protected bump/free-list first; per-thread allocation buffers only if profiles demand it. SharedStructType(fields)creates an immutable layout (field names interned into a shared, immutable name table); instances store a layout pointer andn64-bit field slots. Loads and stores are relaxed 64-bit atomics, which matches the SharedArrayBuffer memory model.- Storable values follow V8. Strings are copied into an immutable shared string region on store (flattened; ropes never enter shared space), so a shared field never points into an isolate heap and no cross-heap write barrier is needed.
- Property access from JS goes through exotic ops on the struct object
(
ANT_EXOTICpath), so ordinary objects keep their fast paths untouched. - GC: isolates treat shared offsets as leaves. A shared collection requests a
global safepoint through the existing
interrupt_pendingmechanism, each isolate reports the shared offsets it reaches from its roots and heap, then one thread marks and sweeps the shared arena and releases the isolates. Triggered by shared-allocation volume, never by an isolate's own GC. - Acceptance: Node
--harmony-structparity on the semantics listed above; identity acrosspostMessage; a GC stress test with threads holding and dropping references; TSan and ASan.
4. SharedArray #
- Fixed-length shared element storage in the same arena with the same value
rules;
lengthimmutable, out-of-range stores throw. Atomics.load/store/exchange/compareExchange/addextended to struct fields andSharedArrayelements (WebKit's object-plus-keyAtomicsidea), withAtomics.wait/notifyon them for lock-free algorithms.
5. JIT fast paths #
- Struct layouts are immutable, so a property IC keyed by layout pointer resolves to a fixed slot offset forever; the template JIT and opt tier emit a layout compare plus one load/store. No transitions, no watchpoints.
- Acceptance: a microbenchmark of field updates under
Lockand a lock-freeAtomics.addcounter; no change on the non-shared gauntlet.
6. Research: shared-heap Thread (closures across threads) #
Only if phases 1–5 show demand for capturing closures. Go/no-go needs a prototype with at most 1% serial overhead on the gauntlet. Prerequisites:
- Split
ant_tinto a heap (ant_heap_t: arenas, object list, shapes, intern table, GC) and a per-thread context (vm, exception, call state). - Multi-mutator GC: per-thread allocation buffers in the fixed arenas, safepoints at back-edges and allocation, conservative scanning of every thread stack.
- Object model: owner-thread tags in the high bits of
overflow_prop(TTL), a segmented overflow layout for objects reshaped by a non-owner, CAS on array storage growth, dictionary-mode reads under a shape lock. - Per-code-unit "ran on one thread" bits, buffered IC/feedback updates flushed at safepoints, and thread-safe rope flattening and interning.
Decisions #
- Build on isolates plus shared objects, not a shared heap; keep the WebKit
Thread/Lock/ConditionAPI on top. - Follow V8's experimental shared-struct semantics where they exist so code
can move between Ant and Node
--harmony-struct. - Shared objects never reference isolate heaps; that is what keeps ordinary code free of new barriers and checks.
Open Questions #
ant:threadmodule (recommended, avoids a non-standard global) or a globalThread.- Whether
SharedStructTypeandSharedArraystay global like V8 or move underant:threaduntil TC39 settles theshared structsyntax. - Isolate pooling for
Threadstartup cost.
Risks #
- Shared-space GC pauses stop every isolate; keep collections rare and measure pause time with many threads.
- Struct field access through exotic ops is slower than ordinary objects until phase 5.
- String copying on store costs memory for large strings; revisit with shared string interning if profiles show it.
Validation Plan #
- Per phase: focused tests in
tests/, parity runs againstnode --harmony-structwhere the API exists, TSan for every thread test, ASan for the shared arena and GC, and the GC gauntlet for regressions.