Canonical Tracker
This is the single canonical issue for JavaScript multithreading in zig-js. Older duplicate roadmap issues should point here instead of tracking similar work in parallel.
Authoritative design and status docs live in docs/threads:
docs/threads/index.md - start here and support matrix.
docs/threads/api.md - current shared-realm thread API.
docs/threads/memory-model.md - JS program races, engine-state races, and the TSan suppression boundary.
docs/threads/testing.md - exact Zig 0.17-dev verification commands.
docs/threads/production-readiness.md - no-GIL production-readiness status.
docs/threads/limits.md - unsupported surfaces and remaining roadmap.
docs/threads/bindings.md - mutable-state rulings.
docs/threads/P7-gc-design.md, docs/threads/P7-gil-removal.md, and docs/threads/P8-structs.md - GC, no-GIL, and TC39 structs planning.
Current State
- Layer A isolated agents / workers / shared memory are implemented: real OS-thread agents, retained
SharedArrayBuffer storage, typed-array Atomics.wait / notify / waitAsync, structured clone, ArrayBuffer transfer/detach, and Worker APIs are covered by unit and conformance tests.
- Shared-realm
Thread is implemented behind Context.createWith(.{ .enable_threads = true }): Thread, Lock, Condition, ThreadLocal, ConcurrentAccessError, property-mode Atomics.*, and proposal-aligned Atomics.Mutex / Atomics.Condition entry points.
enable_threads runs shared-realm Threads true-parallel by default over the GC-managed, thread-safe heap. The serialized fallback is explicit: Context.createWith(.{ .enable_threads = true, .gil = true }).
- The C API exposes the same choice through
ZJSGlobalContextCreateThreaded(gil). Non-threaded contexts remain single-threaded and keep the original affinity rules.
- Shape transitions, named-property metadata and slots, indexed storage, environments, promises, microtasks, inline caches, thread records, waiter queues, shared-buffer storage, host-side thread queues, protected C-API handles, release-function lock records, contended
Lock.hold native callback roots, thread-owned native private-data tags, and GC roots/barriers all have explicit synchronization and tracing paths.
- VM-recursive bytecode calls, generator resumes, async function drives, and async-generator resumes now use the same catchable stack guard as tree-walker calls, so bytecode recursion raises
RangeError before native stack overflow. The deep PR-249 semantics/stack-overflow-per-thread.js witness remains reference-only until calls are iterative/trampolined or run on a real VM stack that can reach the expected thousands-of-frames pre-overflow depth.
Lock.asyncHold() ordering is covered, including the no-fn immediate-grant exclusion from synchronous Lock.hold() until the release function is delivered and called, and the deterministic barging witness where a sync hold legally overtakes a queued no-fn async ticket before await delivers its release function.
- Spawned threads settle their own typed-array
Atomics.waitAsync waiter list before publishing normal completion, so Thread.asyncJoin() can assimilate thread-returned waitAsync promises. Spawned-thread error/termination unwinds abandon child-owned typed-array waitAsync tickets before the child interpreter's stack-owned waiter-list token disappears.
- JS-level races are documented separately from engine races: JS program state can still race without synchronization, while engine-state races are bugs and remain TSan-gated. The TSan suppression boundary is documented in
docs/threads/memory-model.md and guarded by the suppression witness.
- The WebKit PR-249 thread allowlist is currently 225/225 green. Remaining reference-only files are tracked in
docs/threads/testing.md with concrete reasons, and zig build threads-reference-audit fails if any non-allowlisted executable file lacks a blocker classification; python3 tools/threads-reference-audit.py --probe-candidates lists the closest focused promotion probes, python3 tools/threads-reference-audit.py --run-probes executes them with per-case timeouts and prints focused runner evidence before the Zig build tail before allowlist changes, python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60 is the maintained negative baseline for the nearest blockers, and python3 tools/threads-reference-audit.py --format json emits machine-readable counts, blocker categories, promotion probe commands, expected current blocker evidence, and optional live probe results for CI/reporting/issue updates.
- The default PR-249 waiter-storm drain now reports async progress and corpus
print() output on failure. cve/mc-dos-waiter-table-storm.js is stable in the full default corpus and in the focused no-GIL path. Its no-GIL gate covers property Atomics.waitAsync tickets that a peer removes from the global table while the owning spawned thread is closing its stack-local microtask queue; late settlements now reroute to the realm queue instead of stranding reactions after the owner's final flush.
- CI gates every PR and main push with unit/corpus coverage plus no-GIL checks: TSan unit gates, a sharded no-GIL PR-249 corpus TSan sweep, suppression-narrowness witness, test262-parallel representative slice, and seeded concurrent-JS fuzzing (
threadfuzz, TSan fuzzer, amplified fuzzer, broad semantic fuzzer, mid-script-GC wait-pump/microtask/property-waitAsync-late-settlement/creator-buffer/ThreadLocal-finalization/Thread.restrict-finalization/sync-wait-cleanup/promise/teardown/Worker-SAB/Worker-exception/Worker-close/weak-collection fuzzer, lifecycle fuzzer, ReleaseSafe fuzzer, deterministic-result verifier).
- The lifecycle fuzzer covers parked/unjoined Thread teardown, Worker/thread overlap, module Worker/thread overlap, module Worker termination with shared-realm teardown/reaction/cleanup, Worker/thread/finalization scheduling on one retained SAB, Worker termination plus exact shared-realm finalization cleanup, Worker termination while top-level failure tears down parked shared-realm
Threads, pending asyncJoin rejection reactions, and already-ready cleanup jobs on the same retained SAB, exact script/module Worker close/terminate/postMessage drain/drop ordering, Worker handler-exception recovery, Worker handler-exception recovery composed with shared-realm Thread finalization cleanup on one retained SAB, module Worker handler-exception recovery composed with the same retained-SAB cleanup oracle, Thread.restrict lifecycle isolation, Thread exception identity through join() / asyncJoin() while waiters are parked, thread-returned typed-array waitAsync promise assimilation through join() / asyncJoin(), property Atomics.waitAsync late settlement while a peer removes timeout tickets and the owning Thread closes its stack-local microtask queue, child-returned fulfilled/rejected promises and user thenables published through both join() and asyncJoin() with correct assimilation, thrown-object publication with nested promise roots, cross-thread FinalizationRegistry cleanup oracles, cleanup delivery interleaved with join() / asyncJoin() plus unregister-token suppression, cleanup delivery after parked property/condition waiters resume, typed-array waitAsync settlement interleaved with asyncJoin reactions and exact finalization cleanup, Condition.asyncWait reacquire delivery interleaved with join() / asyncJoin() reactions and exact finalization cleanup, proposal-style Atomics.Mutex / Atomics.Condition.waitFor token waiters now take both notify and timeout paths and Atomics.Mutex.lockIfAvailable token waiters now take both acquire-after-release and timeout paths with reused tokens while asyncJoin observers and exact cleanup share the same lifecycle window, teardown termination with pending asyncJoin rejection reactions and child-owned typed-array waitAsync ticket abandonment, teardown termination while property waitAsync timeout compaction, async condition reacquire, a pending asyncJoin rejection reaction, and already-ready FinalizationRegistry cleanup jobs share the same realm turn, Worker termination composed with condition async reacquire, pending asyncJoin rejection cleanup, and exact FinalizationRegistry cleanup, script and module Worker termination composed with child-owned typed-array waitAsync ticket abandonment, pending asyncJoin rejection cleanup, and exact FinalizationRegistry cleanup, Lock.asyncHold() barging, no-fn Lock.asyncHold() release-function delivery while property/condition waiters stay parked before exact cleanup after they resume, Promise reaction queue churn from with-fn Lock.asyncHold, no-fn release-function delivery, typed-array waitAsync, Thread.asyncJoin, and exact FinalizationRegistry cleanup, Lock.asyncHold(fn) throw/release ordering with queued no-fn release grants and exact FinalizationRegistry cleanup, creator-owned SharedArrayBuffer and ArrayBuffer storage that survives the creating Thread's exit, sibling-thread reads, GC pressure, post-creator ArrayBuffer.transfer(), and isolated Worker structured-clone validation after the creator Thread exits plus sibling script Worker and module Worker clone/finalization cleanup/transfer observer variants, Thread.restrict-owned FinalizationRegistry cleanup after owner-thread exit, ThreadLocal isolation, ThreadLocal finalization cleanup, and parent-created child Threads whose asyncJoin() promises outlive the parent Thread's local queue before child release, nested ThreadLocal root checks, rerouted async settlement, and exact finalization cleanup after both thread layers exit. Each seed runs 43 deterministic lifecycle subprograms.
- The mid-script GC fuzzer now runs 22 deterministic subprograms per seed. The wait-pump subprogram blocks peers in property
Atomics.wait, Condition.wait, and contended Lock acquisition while allocation pressure drives parallel_midscript_gc; it exercises queued async-hold grants including a root-bearing rejected grant, async condition reacquire, typed-array waitAsync waiter/reaction roots, pending Thread.asyncJoin fulfillment/rejection reaction roots, ThreadLocal hidden roots, completed-but-unjoined Thread result/exception roots, and exact FinalizationRegistry cleanup with unregister-token suppression. The ThreadLocal-finalization subprogram parks owner threads with targets reachable only through ThreadLocal.value, drives a finishing mid-script sweep, verifies cleanup is not delivered while those hidden roots are live, then clears the values and requires exact cleanup count/sum delivery. The Thread.restrict-finalization subprogram parks owner threads with restricted owner-local objects registered for finalization, verifies nested foreign reads still throw ConcurrentAccessError, drives a finishing mid-script sweep, rejects early cleanup while those owner-thread roots are live, then releases the owners and requires exact asyncJoin and cleanup oracles. The sync-wait cleanup subprogram parks peers in property Atomics.wait, Condition.wait, and contended Lock.hold acquisition through a finishing sweep, verifies each resumed peer's stack root plus exact FinalizationRegistry cleanup count/sum delivery, lets expired property waitAsync tickets compact while those sync peers are parked, keeps one live property waitAsync ticket rooted through the sweep, keeps an isolated Worker parked on a retained SharedArrayBuffer through the same sweep, then notifies/releases both and verifies exact captured-root scoring plus the Worker reply. The sync-wait burst subprogram parks multiple waiters on the same property, the same Condition, and the same contended Lock through a finishing sweep, rejects early cleanup while those stack roots are live, then releases all three wait sets and verifies exact cleanup after quiescence. The sync-timeout exit subprogram parks property Atomics.wait peers and static Atomics.Condition.waitFor peers through a finishing sweep, rejects early cleanup while their stack roots are live, then requires timeout results, Atomics.Mutex.UnlockToken reacquisition/unlock, and exact cleanup after quiescence. The Atomics.Mutex.lockIfAvailable subprogram parks acquire-after-release and timeout token waiters behind a holder through a finishing sweep, rejects early cleanup while their stack roots are live, then requires reused-token acquire and timeout results plus exact cleanup after quiescence. The static Atomics.Condition.wait subprogram parks notify/reacquire token waiters through a finishing sweep, rejects early cleanup while their stack roots are live, then requires exact notify counts, token reacquisition, Thread.asyncJoin() observers, and exact cleanup after quiescence. The async-hold release/waiter cleanup subprogram delivers no-fn Lock.asyncHold() release functions while property and condition waiters stay parked, drives a finishing mid-script parallel sweep before those waiters resume, then verifies exact FinalizationRegistry cleanup count/sum delivery after quiescence. The nested parent/child asyncJoin cleanup subprogram parks parent and child Threads with ThreadLocal roots, child asyncJoin() promises, child Thread completion records, and exact cleanup targets live through a finishing sweep, then verifies parent release, child release, rerouted asyncJoin reactions, and exact cleanup count/sum delivery after quiescence. The promise-publication subprogram leaves a child-returned typed-array waitAsync promise, a child-returned rejected promise, a child-returned user thenable, and a child-thrown object rooted through thread completion/native waiter state across a finishing sweep, then verifies post-sweep Thread.asyncJoin() fulfillment/rejection/thenable assimilation, Thread.join() returning the child promise/thenable for post-completion observers, thrown-object publication, and nested thrown/rejected promises. The property Atomics.waitAsync late-settlement subprogram registers finite-timeout property tickets in child Threads, drives a finishing mid-script sweep after the child local queues have closed, then requires rerouted timeout settlement to reach both asyncJoin() and join() promise observers with exact root/score oracles. The pending-microtask subprogram queues Promise, typed-array waitAsync, Thread.asyncJoin, with-fn Lock.asyncHold, no-fn release-function, and FinalizationRegistry cleanup roots through a finishing mid-script sweep, then drains the realm run loop and verifies exact reaction and cleanup oracles. The creator-owned buffer subprogram leaves child-created SharedArrayBuffer and ArrayBuffer storage rooted through unjoined Thread completion records and delayed asyncJoin observers across a finishing sweep, then verifies blocking join(), post-sweep asyncJoin(), and ArrayBuffer.transfer() observers see exact contents after the creating thread has exited. The script Worker/SAB and module Worker/SAB cleanup subprograms run isolated Workers on the same retained SharedArrayBuffer while shared-realm Threads register cleanup targets and park stack roots through a finishing sweep, then verify exact Worker progress, joined thread roots, asyncJoin reactions, and cleanup count/sum; sibling script/module Worker handler-exception cleanup subprograms first recover from an expected thrown onmessage delivery, then prove the same Worker progress and cleanup oracle through the finishing sweep. Script/module Worker close/terminate subprograms preserve exact FIFO drain/drop, post-close drop, post-terminate receive silence, joined roots, asyncJoin reactions, and cleanup count/sum through the same finishing sweep. The weak-collection subprogram parks property Atomics.wait, Condition.wait, and contended Lock.hold peers while live WeakMap values are reachable only through live weak keys, dead WeakMap/WeakSet targets are reachable only through weak structures and WeakRefs, and FinalizationRegistry unregister-token records compact through a finishing sweep, then verifies live ephemeron values, cleared dead refs, exact cleanup count/sum, and exact unregister suppression. The teardown subprogram parks children after installing child-owned typed-array waitAsync tickets, drives a finishing mid-script parallel sweep, then parent failure triggers teardown; it verifies pending asyncJoin rejection reactions with captured roots and proves post-termination notify wakes zero leaked child waitAsync tickets.
Work Still Tracked Here
- Keep
docs/threads and this issue aligned whenever thread behavior, counts, blockers, or public API wording changes.
- Promote PR-249 files only when the engine implements the behavior and the file passes reliably under Zig
0.17-dev; keep tools/threads-reference-audit.py accurate whenever the reference-only tail changes. The nearest-probe negative baseline now fails if one of the closest reference-only files unexpectedly passes or changes failure shape, and the JSON audit output exposes the same counts, blocker categories, probe commands, expected blocker evidence, and optional live probe results without scraping human-readable logs, so use those signals to promote, reclassify, or update docs rather than leaving stale blocker text in place.
- Improve GC performance: the first GC cell-slab allocation fast path is landed, fresh slab chunks lazily bump cells with a per-bucket bump hint instead of pre-linking every unused slot, a per-bucket fresh-chunk cursor skips already-exhausted bump chunks, the object-sized 1024/2048-byte buckets now use larger slab chunks so empty-context object-cell chunks drop from 17 to 5 and the object-heavy profile drops from roughly 330 chunks to 84, per-bucket free, capacity, and issued-slot counters keep profile/stat snapshots from walking every freed cell or slab chunk, slab ownership lookup is bucket-local with address-span rejects and per-bucket recent-chunk hints before a sorted chunk address index, context destroy skips rebuilding slab freelists for owned cells during bulk teardown while bucket-shaped delegated side allocations still classify once and free through the wrapped allocator, and non-owned bucket-shaped resize/remap/free paths reuse the classification lock before delegation, object finalization skips the object-backing walker when a reclaimed object has no side stores, live
SharedArrayBuffer retain teardown is regression-covered across arena/no-GIL threaded/.gil = true contexts, single-mutator GC object side stores now bypass the cell-slab classifier while true-parallel JS contexts keep the synchronized backing wrapper, and GC-enabled contexts now group the heap, root-tracing binding, and cell backing into one stable lifecycle allocation instead of three separate GPA objects. zig build gc-profile now splits context lifecycle attribution into create and destroy columns, includes an embedder task lifecycle table, compares live object-heavy destroy against quiescent pre-collection plus post-collection destroy, prints aggregate and per-size-class GC cell-backing attribution for both the intrinsic empty-context footprint and object-heavy allocation runs, and reports GC finalizer attribution for empty-context destroy separately from object-heavy context teardown so remaining global setup, workload finalizer draining, and post-collection destroy cost are visible by cell kind, object backing releases, ArrayBuffer/SAB finalizers, and Promise reactions. Continue nursery/generational work and reduce remaining create/destroy overhead.
- Profile and optimize parallel scaling bottlenecks with
zig build threads-profile: it compares no-GIL and .gil = true timings across independent compute, shared object properties, array append, typed-array Atomics, property Atomics.wait / notify, property Atomics.waitAsync timeout settlement, Condition.wait / notifyAll, single-lock and multi-lock Condition.asyncWait, contended Lock.hold, Lock.asyncHold delivery, observed callback settlement, no-fn release-function delivery, lifecycle churn, and an isolated Worker section. The Worker section now prints separate script and module Worker rows for structured-clone inbox/outbox round-trips, empty receive polling, and spawn/post/receive/join/destroy lifecycle cost, so import-graph startup/lifecycle pressure is visible beside plain source Workers. Empty pumps skip the task lock, async-hold delivery uses FIFO head cursors, retry-front grants use an amortized O(1) front stash, realm task-queue writers publish the atomic pending hint from the locked queue length instead of writer-side atomic RMW, and realm task pumps copy larger bounded FIFO bursts, Condition queues use a FIFO head cursor and canceled sync tombstones, sync notifyAll handoff waits on the waiter's condition ack signal instead of sleeping in fixed 1ms polling chunks, and async-only condition notifications prepare no-fn async regrants after releasing the condition queue mutex, and contiguous same-lock async condition regrants are prepared in fixed-size stack batches so notifyAll does not retake that lock once per async waiter, and ready async-condition reacquire jobs are appended to the realm task queue in FIFO bursts to amortize the shared API lock when notifications wake multiple lock groups, condition notify uses one pre-sized wake list for sync/async wake bookkeeping instead of separate per-kind lists and uses a pending-sync countdown for notifyAll handoff completion instead of rescanning the wake list, Promise microtask drains use a FIFO head cursor instead of front-shifting reaction queues, async-hold task pumps snapshot the microtask enqueue generation around each delivered grant so unobserved grants skip empty no-GIL microtask drains, no-fn async-hold grants embed their once-only release state in the already arena-lived hold job to avoid an extra small allocation per delivered release function, property-mode Atomics.notify / sync wait timeout/termination cleanup / waitAsync timeout and teardown paths compact/scan in one pass, Worker channels and $262.agent report queues use FIFO head cursors, empty Worker.receive(..., 0) polls skip drained-queue compaction, unordered root lists use swap removal for active interpreters, protected C-API handles, and GIL park records, WeakMap/WeakSet delete and GC dead-key pruning use unordered tail removal, and FinalizationRegistry unregister uses one stable compaction pass, typed-array Atomics.notify unlinks sync stack tickets before signal, and typed-array waitAsync harvest/abandon paths stable-compact matching tickets in one pass while preserving FIFO order for remaining waiters. threads-profile now also prints joins columns that split Thread.join park/pump iterations out of aggregate parks, lock/cond/prop columns that split the remaining sync park pressure by contended Lock.hold, Condition.wait, and property Atomics.wait, async/done columns for Condition.asyncWait and property waitAsync, and hold/cjob columns that split delivered run-loop jobs into ordinary Lock.asyncHold grants versus Condition.asyncWait reacquire grants, with direct rows for property finite-timeout settlement plus single-lock and multi-lock async condition reacquire delivery; property timeout rows should keep registration and settlement counts equal, the single-lock condition row exposes same-lock regrant batching, the multi-lock condition row exercises FIFO-bursted realm task enqueue across lock groups, lifecycle rows can separate join waiting from other park pressure, and waiter-source and grant-delivery pressure can be read from the same table. Keep using the attribution columns and Worker table to drive reductions in global/environment bindings, object property/element locks, sync waiters, property waitAsync timeout settlement, async condition regrant delivery and its remaining run-loop job cost, user-level locks, lifecycle join waiting, unobserved async-hold grant delivery, promise-observed callback settlement, no-fn release-function delivery, module Worker startup/lifecycle cost, collection helpers, and GC allocation under high thread counts.
- Maintain the memory-model documentation as new synchronization primitives, TSan suppressions, and corpus coverage land.
- Mature mid-script parallel GC: sync-wait pump points publish roots for property
Atomics.wait, Condition.wait, and contended Lock acquisition; host-side thread queues (Gil.tasks, per-lock pending async grants, async condition waiters, typed-array waitAsync waiter/reaction roots, pending Thread.asyncJoin promise/reaction roots, ThreadLocal values, Thread.restrict owner-thread finalization targets, thread completion results, protected C-API handles, and release-function lock records) trace or barrier hidden JS values; contended Lock.hold receiver/callback pairs are temp-rooted while native acquisition parks and pumps. Thread-owned native private data is now explicitly tagged before GC tracing and incompatible-receiver checks cast opaque pointers, so untagged host/private-data records are not speculatively probed as thread roots. Pending microtask queues are traced under the same microtask lock used by appends/pops, including spawned-thread local queues during interpreter root publication. Property-mode sync wait tickets are page-allocator owned for the blocking wait, so peer notifications no longer mutate a stack slot that a parallel collector may conservatively scan. The one-seed aggregate zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=1 --summary failures gate now completes locally after TSan-slowdown-sensitive wait oracles were made deadline-independent or TSan-stretched. C-API: JSValueProtect roots survive mid-script parallel GC now protects an otherwise-unrooted C-API object while shared-realm Threads drive a finishing parallel sweep, verifies the object and nested child remain live while protected, then proves the final JSValueUnprotect releases that root. Parked property/condition/contended-lock peers plus finalization cleanup, expired property waitAsync timeout compaction, one live property waitAsync ticket, and an isolated Worker parked on a retained SharedArrayBuffer through the same finishing sweep are now covered by the focused mid-GC sync-wait cleanup subprogram, multiple same-property/same-condition/same-lock waiters parked through a finishing sweep and released in a burst are covered by the mid-GC sync-wait burst subprogram, property Atomics.wait and static Atomics.Condition.waitFor timeout exits with parked stack roots and exact post-timeout cleanup are covered by the mid-GC sync-timeout subprogram, static Atomics.Mutex.lockIfAvailable acquire-after-release and timeout token waiters with parked stack roots and exact post-quiescence cleanup are covered by the mid-GC lockIfAvailable subprogram, static Atomics.Condition.wait notify/reacquire token waiters with parked stack roots, exact notify counts, token reacquisition, asyncJoin observers, and exact post-quiescence cleanup are covered by the mid-GC condition-wait subprogram, termination with pending asyncJoin/waitAsync roots is covered by the mid-GC teardown subprogram, isolated script Worker/SAB and module Worker/SAB progress plus script/module Worker handler-exception recovery and script/module Worker close/terminate drain/drop while shared-realm cleanup roots are swept are covered by the mid-GC Worker cleanup subprograms, returned fulfilled/rejected promises, user thenables, and thrown-object publication are covered by the mid-GC promise-publication subprogram, pending Promise/microtask roots across asyncHold callback/release delivery, typed-array waitAsync, Thread.asyncJoin, and cleanup reactions are covered by the mid-GC microtask-churn subprogram, property Atomics.waitAsync late settlement after the registering child queue has closed is covered by the mid-GC property-waitAsync-late-settlement subprogram, no-fn Lock.asyncHold() release-function delivery while property/condition waiters stay parked through a finishing sweep is covered by the mid-GC async-hold release/waiter cleanup subprogram, nested parent/child Thread.asyncJoin cleanup roots, ThreadLocal values, child completion records, and rerouted asyncJoin reactions through parked parent/child release are covered by the mid-GC nested-asyncJoin subprogram, creator-owned SharedArrayBuffer / ArrayBuffer backing storage rooted through unjoined Thread completion and delayed asyncJoin observers is covered by the mid-GC creator-buffer subprogram, ThreadLocal-only FinalizationRegistry targets are covered by the mid-GC ThreadLocal-finalization subprogram, Thread.restrict-owned finalization targets are covered by the mid-GC Thread.restrict-finalization subprogram, and live/dead WeakMap/WeakSet and FinalizationRegistry unregister-token compaction is covered by the mid-GC weak-collection subprogram. Joiners now publish gc_parked only for the actual native condition wait, and requested shell/host GC leaves elected mid-script parallel collectors untouched while threads are live; keep adding more waiter cleanup, deeper cross-thread lifecycle teardown variants, higher-iteration TSan expansion, and additional worker/cleanup combinations.
- Keep expanding
threadfuzz and hand-written stress toward more cross-realm scheduling, teardown ordering variants, richer cleanup/finalization interleavings, proposal-style Atomics sync primitive lifecycles, worker/thread lifecycle, backing-store lifetime hazards, and cross-thread teardown.
- Keep C-API context affinity rules explicit; do not expose test-only host knobs (
parallel_js, parallel_midscript_gc, shell $vm hooks) as stable embedder APIs until they have a real public contract.
- Track TC39
proposal-structs only through this issue and docs/threads/P8-structs.md; do not open another parallel structs/threading tracker.
Verification Gates
Use Zig 0.17-dev:
zig build test
zig build test -Dtest-filter=JSValueProtect
zig build test -Dtest-filter=enable_gc
zig build threads-test
zig build threads-reference-audit
python3 tools/threads-reference-audit.py --probe-candidates
python3 tools/threads-reference-audit.py --format json
python3 tools/threads-reference-audit.py --run-probes --probe-timeout 60
python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60
zig build threads-test -Dthreads-case=api/blocking-gate.js
zig build threads-test -Dthreads-case=api/condition-async-wait.js
zig build threads-test -Dthreads-case=api/lock-hold-termination.js
zig build threads-test -Dthreads-case=atomics/ta-wait-thread-gate.js
zig build threads-test -Dthreads-case=atomics/property-waitasync-timeout.js
zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=sync/condition-wait-notify.js
zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=cve/mc-dos-waiter-table-storm.js
zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=api/lock-async-hold.js
zig build test -Dtsan=true
zig build test -Dtsan=true -Dtest-filter=JSValueProtect
zig build test -Dtsan=true -Dtest-filter=parallel_js
zig build threadfuzz -Dfuzz-iters=20
zig build threadfuzz-bin
./zig-out/bin/threadfuzz propwaitasynclate 5 1
zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=20
zig build threadfuzz -Dfuzz-lifecycle=true -Dfuzz-iters=20
zig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300
zig build threads-profile
zig build gc-profile
bun run docs:build
Thread work should also run focused PR-249 cases for the touched behavior, update allowlist/count docs when promotion is real, keep docs/threads/bindings.md current for every new mutable global or threadlocal state, use python3 tools/threads-reference-audit.py --probe-candidates, python3 tools/threads-reference-audit.py --format json, python3 tools/threads-reference-audit.py --run-probes, and the --expect-current-blockers negative baseline before promotion attempts; failed probes now print focused runner evidence before the Zig build tail, expected blockers are checked by their current failure/timeout shape, parseable audit output is available for automation/reporting, and CI remains the source of truth for the sharded no-GIL corpus TSan sweep / suppression witness / test262-parallel legs when they are too expensive locally.
Canonical Tracker
This is the single canonical issue for JavaScript multithreading in zig-js. Older duplicate roadmap issues should point here instead of tracking similar work in parallel.
Authoritative design and status docs live in
docs/threads:docs/threads/index.md- start here and support matrix.docs/threads/api.md- current shared-realm thread API.docs/threads/memory-model.md- JS program races, engine-state races, and the TSan suppression boundary.docs/threads/testing.md- exact Zig0.17-devverification commands.docs/threads/production-readiness.md- no-GIL production-readiness status.docs/threads/limits.md- unsupported surfaces and remaining roadmap.docs/threads/bindings.md- mutable-state rulings.docs/threads/P7-gc-design.md,docs/threads/P7-gil-removal.md, anddocs/threads/P8-structs.md- GC, no-GIL, and TC39 structs planning.Current State
SharedArrayBufferstorage, typed-arrayAtomics.wait/notify/waitAsync, structured clone, ArrayBuffer transfer/detach, and Worker APIs are covered by unit and conformance tests.Threadis implemented behindContext.createWith(.{ .enable_threads = true }):Thread,Lock,Condition,ThreadLocal,ConcurrentAccessError, property-modeAtomics.*, and proposal-alignedAtomics.Mutex/Atomics.Conditionentry points.enable_threadsruns shared-realmThreads true-parallel by default over the GC-managed, thread-safe heap. The serialized fallback is explicit:Context.createWith(.{ .enable_threads = true, .gil = true }).ZJSGlobalContextCreateThreaded(gil). Non-threaded contexts remain single-threaded and keep the original affinity rules.Lock.holdnative callback roots, thread-owned native private-data tags, and GC roots/barriers all have explicit synchronization and tracing paths.RangeErrorbefore native stack overflow. The deep PR-249semantics/stack-overflow-per-thread.jswitness remains reference-only until calls are iterative/trampolined or run on a real VM stack that can reach the expected thousands-of-frames pre-overflow depth.Lock.asyncHold()ordering is covered, including the no-fn immediate-grant exclusion from synchronousLock.hold()until the release function is delivered and called, and the deterministic barging witness where a sync hold legally overtakes a queued no-fn async ticket beforeawaitdelivers its release function.Atomics.waitAsyncwaiter list before publishing normal completion, soThread.asyncJoin()can assimilate thread-returned waitAsync promises. Spawned-thread error/termination unwinds abandon child-owned typed-arraywaitAsynctickets before the child interpreter's stack-owned waiter-list token disappears.docs/threads/memory-model.mdand guarded by the suppression witness.docs/threads/testing.mdwith concrete reasons, andzig build threads-reference-auditfails if any non-allowlisted executable file lacks a blocker classification;python3 tools/threads-reference-audit.py --probe-candidateslists the closest focused promotion probes,python3 tools/threads-reference-audit.py --run-probesexecutes them with per-case timeouts and prints focused runner evidence before the Zig build tail before allowlist changes,python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60is the maintained negative baseline for the nearest blockers, andpython3 tools/threads-reference-audit.py --format jsonemits machine-readable counts, blocker categories, promotion probe commands, expected current blocker evidence, and optional live probe results for CI/reporting/issue updates.print()output on failure.cve/mc-dos-waiter-table-storm.jsis stable in the full default corpus and in the focused no-GIL path. Its no-GIL gate covers propertyAtomics.waitAsynctickets that a peer removes from the global table while the owning spawned thread is closing its stack-local microtask queue; late settlements now reroute to the realm queue instead of stranding reactions after the owner's final flush.threadfuzz, TSan fuzzer, amplified fuzzer, broad semantic fuzzer, mid-script-GC wait-pump/microtask/property-waitAsync-late-settlement/creator-buffer/ThreadLocal-finalization/Thread.restrict-finalization/sync-wait-cleanup/promise/teardown/Worker-SAB/Worker-exception/Worker-close/weak-collection fuzzer, lifecycle fuzzer, ReleaseSafe fuzzer, deterministic-result verifier).Threads, pendingasyncJoinrejection reactions, and already-ready cleanup jobs on the same retained SAB, exact script/module Worker close/terminate/postMessage drain/drop ordering, Worker handler-exception recovery, Worker handler-exception recovery composed with shared-realm Thread finalization cleanup on one retained SAB, module Worker handler-exception recovery composed with the same retained-SAB cleanup oracle,Thread.restrictlifecycle isolation, Thread exception identity throughjoin()/asyncJoin()while waiters are parked, thread-returned typed-arraywaitAsyncpromise assimilation throughjoin()/asyncJoin(), propertyAtomics.waitAsynclate settlement while a peer removes timeout tickets and the owning Thread closes its stack-local microtask queue, child-returned fulfilled/rejected promises and user thenables published through bothjoin()andasyncJoin()with correct assimilation, thrown-object publication with nested promise roots, cross-threadFinalizationRegistrycleanup oracles, cleanup delivery interleaved withjoin()/asyncJoin()plus unregister-token suppression, cleanup delivery after parked property/condition waiters resume, typed-arraywaitAsyncsettlement interleaved withasyncJoinreactions and exact finalization cleanup,Condition.asyncWaitreacquire delivery interleaved withjoin()/asyncJoin()reactions and exact finalization cleanup, proposal-styleAtomics.Mutex/Atomics.Condition.waitFortoken waiters now take both notify and timeout paths andAtomics.Mutex.lockIfAvailabletoken waiters now take both acquire-after-release and timeout paths with reused tokens whileasyncJoinobservers and exact cleanup share the same lifecycle window, teardown termination with pendingasyncJoinrejection reactions and child-owned typed-arraywaitAsyncticket abandonment, teardown termination while propertywaitAsynctimeout compaction, async condition reacquire, a pendingasyncJoinrejection reaction, and already-readyFinalizationRegistrycleanup jobs share the same realm turn, Worker termination composed with condition async reacquire, pendingasyncJoinrejection cleanup, and exactFinalizationRegistrycleanup, script and module Worker termination composed with child-owned typed-arraywaitAsyncticket abandonment, pendingasyncJoinrejection cleanup, and exactFinalizationRegistrycleanup,Lock.asyncHold()barging, no-fnLock.asyncHold()release-function delivery while property/condition waiters stay parked before exact cleanup after they resume, Promise reaction queue churn from with-fnLock.asyncHold, no-fn release-function delivery, typed-arraywaitAsync,Thread.asyncJoin, and exactFinalizationRegistrycleanup,Lock.asyncHold(fn)throw/release ordering with queued no-fn release grants and exactFinalizationRegistrycleanup, creator-ownedSharedArrayBufferandArrayBufferstorage that survives the creating Thread's exit, sibling-thread reads, GC pressure, post-creatorArrayBuffer.transfer(), and isolated Worker structured-clone validation after the creator Thread exits plus sibling script Worker and module Worker clone/finalization cleanup/transfer observer variants,Thread.restrict-ownedFinalizationRegistrycleanup after owner-thread exit,ThreadLocalisolation,ThreadLocalfinalization cleanup, and parent-created childThreads whoseasyncJoin()promises outlive the parent Thread's local queue before child release, nestedThreadLocalroot checks, rerouted async settlement, and exact finalization cleanup after both thread layers exit. Each seed runs 43 deterministic lifecycle subprograms.Atomics.wait,Condition.wait, and contendedLockacquisition while allocation pressure drivesparallel_midscript_gc; it exercises queued async-hold grants including a root-bearing rejected grant, async condition reacquire, typed-arraywaitAsyncwaiter/reaction roots, pendingThread.asyncJoinfulfillment/rejection reaction roots,ThreadLocalhidden roots, completed-but-unjoined Thread result/exception roots, and exactFinalizationRegistrycleanup with unregister-token suppression. The ThreadLocal-finalization subprogram parks owner threads with targets reachable only throughThreadLocal.value, drives a finishing mid-script sweep, verifies cleanup is not delivered while those hidden roots are live, then clears the values and requires exact cleanup count/sum delivery. The Thread.restrict-finalization subprogram parks owner threads with restricted owner-local objects registered for finalization, verifies nested foreign reads still throwConcurrentAccessError, drives a finishing mid-script sweep, rejects early cleanup while those owner-thread roots are live, then releases the owners and requires exact asyncJoin and cleanup oracles. The sync-wait cleanup subprogram parks peers in propertyAtomics.wait,Condition.wait, and contendedLock.holdacquisition through a finishing sweep, verifies each resumed peer's stack root plus exactFinalizationRegistrycleanup count/sum delivery, lets expired propertywaitAsynctickets compact while those sync peers are parked, keeps one live propertywaitAsyncticket rooted through the sweep, keeps an isolated Worker parked on a retainedSharedArrayBufferthrough the same sweep, then notifies/releases both and verifies exact captured-root scoring plus the Worker reply. The sync-wait burst subprogram parks multiple waiters on the same property, the sameCondition, and the same contendedLockthrough a finishing sweep, rejects early cleanup while those stack roots are live, then releases all three wait sets and verifies exact cleanup after quiescence. The sync-timeout exit subprogram parks propertyAtomics.waitpeers and staticAtomics.Condition.waitForpeers through a finishing sweep, rejects early cleanup while their stack roots are live, then requires timeout results,Atomics.Mutex.UnlockTokenreacquisition/unlock, and exact cleanup after quiescence. TheAtomics.Mutex.lockIfAvailablesubprogram parks acquire-after-release and timeout token waiters behind a holder through a finishing sweep, rejects early cleanup while their stack roots are live, then requires reused-token acquire and timeout results plus exact cleanup after quiescence. The staticAtomics.Condition.waitsubprogram parks notify/reacquire token waiters through a finishing sweep, rejects early cleanup while their stack roots are live, then requires exact notify counts, token reacquisition,Thread.asyncJoin()observers, and exact cleanup after quiescence. The async-hold release/waiter cleanup subprogram delivers no-fnLock.asyncHold()release functions while property and condition waiters stay parked, drives a finishing mid-script parallel sweep before those waiters resume, then verifies exactFinalizationRegistrycleanup count/sum delivery after quiescence. The nested parent/child asyncJoin cleanup subprogram parks parent and childThreads withThreadLocalroots, childasyncJoin()promises, childThreadcompletion records, and exact cleanup targets live through a finishing sweep, then verifies parent release, child release, rerouted asyncJoin reactions, and exact cleanup count/sum delivery after quiescence. The promise-publication subprogram leaves a child-returned typed-arraywaitAsyncpromise, a child-returned rejected promise, a child-returned user thenable, and a child-thrown object rooted through thread completion/native waiter state across a finishing sweep, then verifies post-sweepThread.asyncJoin()fulfillment/rejection/thenable assimilation,Thread.join()returning the child promise/thenable for post-completion observers, thrown-object publication, and nested thrown/rejected promises. The propertyAtomics.waitAsynclate-settlement subprogram registers finite-timeout property tickets in child Threads, drives a finishing mid-script sweep after the child local queues have closed, then requires rerouted timeout settlement to reach bothasyncJoin()andjoin()promise observers with exact root/score oracles. The pending-microtask subprogram queues Promise, typed-arraywaitAsync,Thread.asyncJoin, with-fnLock.asyncHold, no-fn release-function, andFinalizationRegistrycleanup roots through a finishing mid-script sweep, then drains the realm run loop and verifies exact reaction and cleanup oracles. The creator-owned buffer subprogram leaves child-createdSharedArrayBufferandArrayBufferstorage rooted through unjoinedThreadcompletion records and delayedasyncJoinobservers across a finishing sweep, then verifies blockingjoin(), post-sweepasyncJoin(), andArrayBuffer.transfer()observers see exact contents after the creating thread has exited. The script Worker/SAB and module Worker/SAB cleanup subprograms run isolated Workers on the same retainedSharedArrayBufferwhile shared-realmThreads register cleanup targets and park stack roots through a finishing sweep, then verify exact Worker progress, joined thread roots, asyncJoin reactions, and cleanup count/sum; sibling script/module Worker handler-exception cleanup subprograms first recover from an expected thrownonmessagedelivery, then prove the same Worker progress and cleanup oracle through the finishing sweep. Script/module Worker close/terminate subprograms preserve exact FIFO drain/drop, post-close drop, post-terminate receive silence, joined roots, asyncJoin reactions, and cleanup count/sum through the same finishing sweep. The weak-collection subprogram parks propertyAtomics.wait,Condition.wait, and contendedLock.holdpeers while live WeakMap values are reachable only through live weak keys, dead WeakMap/WeakSet targets are reachable only through weak structures and WeakRefs, and FinalizationRegistry unregister-token records compact through a finishing sweep, then verifies live ephemeron values, cleared dead refs, exact cleanup count/sum, and exact unregister suppression. The teardown subprogram parks children after installing child-owned typed-arraywaitAsynctickets, drives a finishing mid-script parallel sweep, then parent failure triggers teardown; it verifies pendingasyncJoinrejection reactions with captured roots and proves post-termination notify wakes zero leaked child waitAsync tickets.Work Still Tracked Here
docs/threadsand this issue aligned whenever thread behavior, counts, blockers, or public API wording changes.0.17-dev; keeptools/threads-reference-audit.pyaccurate whenever the reference-only tail changes. The nearest-probe negative baseline now fails if one of the closest reference-only files unexpectedly passes or changes failure shape, and the JSON audit output exposes the same counts, blocker categories, probe commands, expected blocker evidence, and optional live probe results without scraping human-readable logs, so use those signals to promote, reclassify, or update docs rather than leaving stale blocker text in place.SharedArrayBufferretain teardown is regression-covered across arena/no-GIL threaded/.gil = truecontexts, single-mutator GC object side stores now bypass the cell-slab classifier while true-parallel JS contexts keep the synchronized backing wrapper, and GC-enabled contexts now group the heap, root-tracing binding, and cell backing into one stable lifecycle allocation instead of three separate GPA objects.zig build gc-profilenow splits context lifecycle attribution into create and destroy columns, includes an embedder task lifecycle table, compares live object-heavy destroy against quiescent pre-collection plus post-collection destroy, prints aggregate and per-size-class GC cell-backing attribution for both the intrinsic empty-context footprint and object-heavy allocation runs, and reports GC finalizer attribution for empty-context destroy separately from object-heavy context teardown so remaining global setup, workload finalizer draining, and post-collection destroy cost are visible by cell kind, object backing releases, ArrayBuffer/SAB finalizers, and Promise reactions. Continue nursery/generational work and reduce remaining create/destroy overhead.zig build threads-profile: it compares no-GIL and.gil = truetimings across independent compute, shared object properties, array append, typed-array Atomics, propertyAtomics.wait/notify, propertyAtomics.waitAsynctimeout settlement,Condition.wait/notifyAll, single-lock and multi-lockCondition.asyncWait, contendedLock.hold,Lock.asyncHolddelivery, observed callback settlement, no-fn release-function delivery, lifecycle churn, and an isolated Worker section. The Worker section now prints separate script and module Worker rows for structured-clone inbox/outbox round-trips, empty receive polling, and spawn/post/receive/join/destroy lifecycle cost, so import-graph startup/lifecycle pressure is visible beside plain source Workers. Empty pumps skip the task lock, async-hold delivery uses FIFO head cursors, retry-front grants use an amortized O(1) front stash, realm task-queue writers publish the atomic pending hint from the locked queue length instead of writer-side atomic RMW, and realm task pumps copy larger bounded FIFO bursts, Condition queues use a FIFO head cursor and canceled sync tombstones, syncnotifyAllhandoff waits on the waiter's condition ack signal instead of sleeping in fixed 1ms polling chunks, and async-only condition notifications prepare no-fn async regrants after releasing the condition queue mutex, and contiguous same-lock async condition regrants are prepared in fixed-size stack batches so notifyAll does not retake that lock once per async waiter, and ready async-condition reacquire jobs are appended to the realm task queue in FIFO bursts to amortize the shared API lock when notifications wake multiple lock groups, condition notify uses one pre-sized wake list for sync/async wake bookkeeping instead of separate per-kind lists and uses a pending-sync countdown for notifyAll handoff completion instead of rescanning the wake list, Promise microtask drains use a FIFO head cursor instead of front-shifting reaction queues, async-hold task pumps snapshot the microtask enqueue generation around each delivered grant so unobserved grants skip empty no-GIL microtask drains, no-fn async-hold grants embed their once-only release state in the already arena-lived hold job to avoid an extra small allocation per delivered release function, property-modeAtomics.notify/ sync wait timeout/termination cleanup /waitAsynctimeout and teardown paths compact/scan in one pass, Worker channels and$262.agentreport queues use FIFO head cursors, emptyWorker.receive(..., 0)polls skip drained-queue compaction, unordered root lists use swap removal for active interpreters, protected C-API handles, and GIL park records, WeakMap/WeakSet delete and GC dead-key pruning use unordered tail removal, and FinalizationRegistryunregisteruses one stable compaction pass, typed-arrayAtomics.notifyunlinks sync stack tickets before signal, and typed-arraywaitAsyncharvest/abandon paths stable-compact matching tickets in one pass while preserving FIFO order for remaining waiters.threads-profilenow also printsjoinscolumns that splitThread.joinpark/pump iterations out of aggregate parks,lock/cond/propcolumns that split the remaining sync park pressure by contendedLock.hold,Condition.wait, and propertyAtomics.wait,async/donecolumns forCondition.asyncWaitand propertywaitAsync, andhold/cjobcolumns that split delivered run-loop jobs into ordinaryLock.asyncHoldgrants versusCondition.asyncWaitreacquire grants, with direct rows for property finite-timeout settlement plus single-lock and multi-lock async condition reacquire delivery; property timeout rows should keep registration and settlement counts equal, the single-lock condition row exposes same-lock regrant batching, the multi-lock condition row exercises FIFO-bursted realm task enqueue across lock groups, lifecycle rows can separate join waiting from other park pressure, and waiter-source and grant-delivery pressure can be read from the same table. Keep using the attribution columns and Worker table to drive reductions in global/environment bindings, object property/element locks, sync waiters, propertywaitAsynctimeout settlement, async condition regrant delivery and its remaining run-loop job cost, user-level locks, lifecycle join waiting, unobserved async-hold grant delivery, promise-observed callback settlement, no-fn release-function delivery, module Worker startup/lifecycle cost, collection helpers, and GC allocation under high thread counts.Atomics.wait,Condition.wait, and contendedLockacquisition; host-side thread queues (Gil.tasks, per-lock pending async grants, async condition waiters, typed-arraywaitAsyncwaiter/reaction roots, pendingThread.asyncJoinpromise/reaction roots, ThreadLocal values, Thread.restrict owner-thread finalization targets, thread completion results, protected C-API handles, and release-function lock records) trace or barrier hidden JS values; contendedLock.holdreceiver/callback pairs are temp-rooted while native acquisition parks and pumps. Thread-owned native private data is now explicitly tagged before GC tracing and incompatible-receiver checks cast opaque pointers, so untagged host/private-data records are not speculatively probed as thread roots. Pending microtask queues are traced under the same microtask lock used by appends/pops, including spawned-thread local queues during interpreter root publication. Property-mode sync wait tickets are page-allocator owned for the blocking wait, so peer notifications no longer mutate a stack slot that a parallel collector may conservatively scan. The one-seed aggregatezig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=1 --summary failuresgate now completes locally after TSan-slowdown-sensitive wait oracles were made deadline-independent or TSan-stretched.C-API: JSValueProtect roots survive mid-script parallel GCnow protects an otherwise-unrooted C-API object while shared-realmThreads drive a finishing parallel sweep, verifies the object and nested child remain live while protected, then proves the finalJSValueUnprotectreleases that root. Parked property/condition/contended-lock peers plus finalization cleanup, expired propertywaitAsynctimeout compaction, one live propertywaitAsyncticket, and an isolated Worker parked on a retainedSharedArrayBufferthrough the same finishing sweep are now covered by the focused mid-GC sync-wait cleanup subprogram, multiple same-property/same-condition/same-lock waiters parked through a finishing sweep and released in a burst are covered by the mid-GC sync-wait burst subprogram, propertyAtomics.waitand staticAtomics.Condition.waitFortimeout exits with parked stack roots and exact post-timeout cleanup are covered by the mid-GC sync-timeout subprogram, staticAtomics.Mutex.lockIfAvailableacquire-after-release and timeout token waiters with parked stack roots and exact post-quiescence cleanup are covered by the mid-GC lockIfAvailable subprogram, staticAtomics.Condition.waitnotify/reacquire token waiters with parked stack roots, exact notify counts, token reacquisition, asyncJoin observers, and exact post-quiescence cleanup are covered by the mid-GC condition-wait subprogram, termination with pending asyncJoin/waitAsync roots is covered by the mid-GC teardown subprogram, isolated script Worker/SAB and module Worker/SAB progress plus script/module Worker handler-exception recovery and script/module Worker close/terminate drain/drop while shared-realm cleanup roots are swept are covered by the mid-GC Worker cleanup subprograms, returned fulfilled/rejected promises, user thenables, and thrown-object publication are covered by the mid-GC promise-publication subprogram, pending Promise/microtask roots across asyncHold callback/release delivery, typed-arraywaitAsync,Thread.asyncJoin, and cleanup reactions are covered by the mid-GC microtask-churn subprogram, propertyAtomics.waitAsynclate settlement after the registering child queue has closed is covered by the mid-GC property-waitAsync-late-settlement subprogram, no-fnLock.asyncHold()release-function delivery while property/condition waiters stay parked through a finishing sweep is covered by the mid-GC async-hold release/waiter cleanup subprogram, nested parent/childThread.asyncJoincleanup roots, ThreadLocal values, child completion records, and rerouted asyncJoin reactions through parked parent/child release are covered by the mid-GC nested-asyncJoin subprogram, creator-ownedSharedArrayBuffer/ArrayBufferbacking storage rooted through unjoined Thread completion and delayedasyncJoinobservers is covered by the mid-GC creator-buffer subprogram, ThreadLocal-only FinalizationRegistry targets are covered by the mid-GC ThreadLocal-finalization subprogram, Thread.restrict-owned finalization targets are covered by the mid-GC Thread.restrict-finalization subprogram, and live/dead WeakMap/WeakSet and FinalizationRegistry unregister-token compaction is covered by the mid-GC weak-collection subprogram. Joiners now publishgc_parkedonly for the actual native condition wait, and requested shell/host GC leaves elected mid-script parallel collectors untouched while threads are live; keep adding more waiter cleanup, deeper cross-thread lifecycle teardown variants, higher-iteration TSan expansion, and additional worker/cleanup combinations.threadfuzzand hand-written stress toward more cross-realm scheduling, teardown ordering variants, richer cleanup/finalization interleavings, proposal-style Atomics sync primitive lifecycles, worker/thread lifecycle, backing-store lifetime hazards, and cross-thread teardown.parallel_js,parallel_midscript_gc, shell$vmhooks) as stable embedder APIs until they have a real public contract.proposal-structsonly through this issue anddocs/threads/P8-structs.md; do not open another parallel structs/threading tracker.Verification Gates
Use Zig
0.17-dev:Thread work should also run focused PR-249 cases for the touched behavior, update allowlist/count docs when promotion is real, keep
docs/threads/bindings.mdcurrent for every new mutable global or threadlocal state, usepython3 tools/threads-reference-audit.py --probe-candidates,python3 tools/threads-reference-audit.py --format json,python3 tools/threads-reference-audit.py --run-probes, and the--expect-current-blockersnegative baseline before promotion attempts; failed probes now print focused runner evidence before the Zig build tail, expected blockers are checked by their current failure/timeout shape, parseable audit output is available for automation/reporting, and CI remains the source of truth for the sharded no-GIL corpus TSan sweep / suppression witness / test262-parallel legs when they are too expensive locally.