Files
9e1233c9df threads: atomics, wasi-threads, and a copy-on-write shared-memory image (#30)
Adds the wasm threads proposal to the codegen and, on top of it, a
copy-on-write memory-sharing facility for embedders that stand up many
instances of the same module.

Threads
  - Lower the 0xFE atomic family (load/store/rmw/cmpxchg across widths and
    sub-word lanes, plus wait/notify) to sync/atomic-based module helpers.
  - wasi_thread_spawn runs each guest thread on a goroutine. A shared memory
    is allocated at its declared ceiling once and never relocates, so every
    agent's cached base pointer stays valid; growth is a lone atomic store.
  - Passive data segments, per-agent globals, coherent plain access for
    shared memories, and multi-package output for threaded modules.
  - A shared memory's ceiling is a runtime value the embedder chooses, not
    the binary's declared maximum.

Copy-on-write shared memory
  - NewWithMemory lets the caller supply the linear memory, and memory.init
    compares before it writes — so a copy-on-write map of a pre-initialized
    image survives the start section instead of being faulted private.
  - NewSharedImage builds an initialized image once per process and maps it
    copy-on-write into every instance: the data segments (identical in every
    instance, read-mostly) then cost almost nothing per instance.
  - NewSharedSnapshot / NewFromSnapshot share a WHOLE initialized instance —
    memory and wasm globals both — so instances re-run nothing at all.
    SaveGlobals/RestoreGlobals carry the initialized state that lives outside
    linear memory (the stack pointer and other mutable globals).

Also: coverage for the threads/atomics helpers, a WASM2GO_SSA_PASSES_OFF
diagnostic knob, an AtomicWait timeout margin, and thread-trap visibility.

Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-07-16 10:37:21 +09:00

46 lines
2.2 KiB
WebAssembly Text Format

;; cg_threads.wat — shared memory + a real wasi_thread_spawn. The guest
;; spawns N agents that each atomically bump a counter and notify; the
;; main agent waits on the counter until they are done. Exercises the
;; goroutine-backed spawn, cross-goroutine atomics on shared memory, and
;; the wait/notify parking lot.
(module
(import "wasi" "thread-spawn" (func $thread_spawn (param i32) (result i32)))
(memory (export "memory") 1 2 shared)
;; wasi-libc's thread entry: called on a fresh agent with (tid, arg).
;; arg is the address of the counter to bump.
(func (export "wasi_thread_start") (param $tid i32) (param $arg i32)
(drop (i32.atomic.rmw.add (local.get $arg) (i32.const 1)))
(drop (memory.atomic.notify (local.get $arg) (i32.const -1))))
;; Spawn n agents on counter address 64 and spin-wait (with a bounded
;; atomic wait, so a lost notify shows up as a timeout, not a hang)
;; until the counter reaches n. Returns the final counter.
(func (export "spawn_and_join") (param $n i32) (result i32)
(local $i i32)
(i32.atomic.store (i32.const 64) (i32.const 0))
(block $spawned
(loop $spawn
(br_if $spawned (i32.ge_s (local.get $i) (local.get $n)))
(drop (call $thread_spawn (i32.const 64)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $spawn)))
(block $done
(loop $wait
(br_if $done (i32.ge_s (i32.atomic.load (i32.const 64)) (local.get $n)))
;; wait while the counter still holds its current value (1s cap)
(drop (memory.atomic.wait32 (i32.const 64)
(i32.atomic.load (i32.const 64))
(i64.const 1000000000)))
(br $wait)))
(i32.atomic.load (i32.const 64)))
;; memory.grow on a shared memory must not relocate: the pointer other
;; agents deref stays valid. Returns old size in pages, then the new one.
(func (export "grow_shared") (result i32)
(i32.atomic.store (i32.const 128) (i32.const 0xbeef))
(drop (memory.grow (i32.const 1)))
;; data written before the grow must still be there, and the new size visible
(i32.add (i32.mul (memory.size) (i32.const 65536))
(i32.atomic.load (i32.const 128)))))