Files
wasm2go/testdata/cg_threads_musl_lock.wat
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

55 lines
2.6 KiB
WebAssembly Text Format

;; musl's 2-int internal lock (__lock/__unlock in src/thread/__lock.c): the
;; futex word at l[0] and a WAITERS COUNTER at l[1]. The unlock side reads the
;; counter with a PLAIN load to decide whether to notify, and the waiter spins
;; on the futex word with PLAIN loads between futex waits. wasm's memory model
;; gives plain accesses hardware-like coherence, so this is legal wasm — the
;; translated Go must preserve it or malloc/stdio locks (and everything else
;; built on __lock) can skip wakes and strand a thread forever.
(module
(import "wasi" "thread-spawn" (func $spawn (param i32) (result i32)))
(memory 1 4 shared)
;; 16: futex word / 20: waiters / 24: shared counter / 32: agent-done
(func $lock
(block $have
(br_if $have
(i32.eqz (i32.atomic.rmw.cmpxchg (i32.const 16) (i32.const 0) (i32.const 1))))
(loop $retry
(if (i32.ne (i32.atomic.rmw.xchg (i32.const 16) (i32.const 2)) (i32.const 0))
(then
;; __wait: waiters++, wait while the PLAIN load still sees 2, waiters--
(drop (i32.atomic.rmw.add (i32.const 20) (i32.const 1)))
(block $awake (loop $w
(br_if $awake (i32.ne (i32.load (i32.const 16)) (i32.const 2)))
(drop (memory.atomic.wait32 (i32.const 16) (i32.const 2) (i64.const -1)))
(br $w)))
(drop (i32.atomic.rmw.sub (i32.const 20) (i32.const 1)))
(br $retry))))))
(func $unlock
;; a_store(l, 0) then wake ONLY IF the PLAIN load of waiters is nonzero
(i32.atomic.store (i32.const 16) (i32.const 0))
(if (i32.ne (i32.load (i32.const 20)) (i32.const 0))
(then (drop (memory.atomic.notify (i32.const 16) (i32.const 1))))))
(func $bump (param $n i32)
(local $i i32)
(block $done (loop $l
(br_if $done (i32.ge_u (local.get $i) (local.get $n)))
(call $lock)
(i32.store (i32.const 24) (i32.add (i32.load (i32.const 24)) (i32.const 1)))
(call $unlock)
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $l))))
(func (export "wasi_thread_start") (param $tid i32) (param $arg i32)
(call $bump (i32.const 50000))
(i32.atomic.store (i32.const 32) (i32.const 1)))
(func (export "run") (result i32)
(local $i i32)
(drop (call $spawn (i32.const 0)))
(call $bump (i32.const 50000))
(block $got (loop $wait
(br_if $got (i32.eq (i32.atomic.load (i32.const 32)) (i32.const 1)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $got (i32.ge_u (local.get $i) (i32.const 2000000000)))
(br $wait)))
(call $lock)
(i32.load (i32.const 24))))