El modelo de threading

WASM threads usa un modelo de memoria compartida similar a pthreads. Múltiples threads comparten la misma memoria lineal. La sincronización pasa a través de operaciones atómicas sobre esa memoria.

// Compilar con: emcc -pthread -s SHARED_MEMORY=1 -s PTHREAD_POOL_SIZE=8
#include <pthread.h>
#include <stdatomic.h>

#define WIDTH 800
#define HEIGHT 600
#define NUM_SAMPLES 64

static uint32_t framebuffer[WIDTH * HEIGHT];
static atomic_int next_row;
static atomic_int completed_rows;

void* render_thread(void* arg) {
 while (1) {
 int row = atomic_fetch_add(&next_row, 1);
 if (row >= HEIGHT) break;

 for (int x = 0; x < WIDTH; x++) {
 float r = 0, g = 0, b = 0;
 for (int s = 0; s < NUM_SAMPLES; s++) {
 Ray ray = camera_ray(x, row, s);
 Vec3 color = trace(ray, scene, 0);
 r += color.x; g += color.y; b += color.z;
 }
 r /= NUM_SAMPLES; g /= NUM_SAMPLES; b /= NUM_SAMPLES;
 framebuffer[row * WIDTH + x] = pack_rgba(r, g, b, 1.0f);
 }
 atomic_fetch_add(&completed_rows, 1);
 }
 return NULL;
}

Integración con el browser: Workers + SharedArrayBuffer

En el browser, cada thread de WASM corre en un Web Worker:

const memory = new WebAssembly.Memory({
 initial: 256, maximum: 1024, shared: true
});

const numThreads = navigator.hardwareConcurrency || 4;

for (let i = 0; i < numThreads; i++) {
 const worker = new Worker('worker.js');
 worker.postMessage({
 type: 'init', wasmBytes, memory,
 threadId: i, totalThreads: numThreads
 });
}

// Leer el framebuffer y renderizar al canvas
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(800, 600);

function renderFrame() {
 const pixels = new Uint8ClampedArray(memory.buffer, 0, 800 * 600 * 4);
 imageData.data.set(pixels);
 ctx.putImageData(imageData, 0, 0);
 requestAnimationFrame(renderFrame);
}
renderFrame();

El requisito crítico: tu página debe configurar headers de seguridad para que SharedArrayBuffer funcione:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Sin estos headers, SharedArrayBuffer se deshabilita y tu memoria compartida no va a funcionar. Este es el gotcha de deploy más común.

Performance: los números reales

Los benchmarks del ray tracer revelan resultados significativos. La escena es una Cornell box con 4 esferas, 64 samples por pixel, 800x600:

C nativo (gcc -O3, 8 threads): 1.240ms
WASM threads (Chrome, 8 workers): 1.890ms (1.52x nativo)
WASM threads (Firefox, 8 workers): 2.100ms (1.69x nativo)
WASM single-thread (Chrome): 9.450ms (7.62x nativo)
JavaScript (optimizado a mano, 8 workers): 14.200ms (11.45x nativo)

WASM threads logra 52-66% del rendimiento nativo. El scaling es casi lineal hasta el conteo de cores físicos:

1 thread: 9.450ms
2 threads: 4.780ms (1.98x speedup)
4 threads: 2.410ms (3.92x speedup)
8 threads: 1.890ms (5.00x speedup)

WASI threads fuera del browser

La propuesta wasi-threads trae el mismo modelo a runtimes WASM del lado servidor:

clang --target=wasm32-wasi-threads \
 -pthread -matomics -mbulk-memory \
 -Wl,--import-memory,--shared-memory,--max-memory=67108864 \
 -o ray_tracer.wasm ray_tracer.c

wasmtime run --wasi threads ray_tracer.wasm

En Rust, el target wasm32-wasi-threads es experimental pero funcional:

use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;

fn main() {
 let counter = Arc::new(AtomicI32::new(0));
 let mut handles = vec![];

 for _ in 0..8 {
 let counter = Arc::clone(&counter);
 let handle = std::thread::spawn(move || {
 for _ in 0..1000 {
 counter.fetch_add(1, Ordering::Relaxed);
 }
 });
 handles.push(handle);
 }

 for handle in handles {
 handle.join().unwrap();
 }
 assert_eq!(counter.load(Ordering::SeqCst), 8000);
}

Primitivas de sincronización

typedef struct { _Atomic int locked; } WasmMutex;

void wasm_mutex_lock(WasmMutex* m) {
 while (1) {
 int expected = 0;
 if (atomic_compare_exchange_weak(&m->locked, &expected, 1)) return;
 __builtin_wasm_memory_atomic_wait32(&m->locked, 1, -1);
 }
}

void wasm_mutex_unlock(WasmMutex* m) {
 atomic_store(&m->locked, 0);
 __builtin_wasm_memory_atomic_notify(&m->locked, 1);
}

El par wait32/notify es crucial. Sin ellos, usarías un spin-lock que quema ciclos de CPU. wait32 le dice al runtime "dormí este thread hasta que el valor en esta dirección de memoria cambie."

Dónde importa esto

WASM threads desbloquean workloads que antes eran impracticables en el browser:

  • Procesamiento de imagen/video: Operaciones paralelas de pixels
  • Computación científica: Simulaciones Monte Carlo, operaciones de matrices
  • Motores de juegos: Simulación de física en threads separados
  • Compresión: Compresión de bloques paralela (zstd, brotli)
  • Inferencia de ML: Multiplicación de matrices paralela para modelos chicos

WASM threads no son un reemplazo completo del threading nativo. Son una aproximación suficientemente buena que trae paralelismo real a ambientes sandboxeados. Para los workloads que se benefician, y el speedup de 5x del ray tracer con 8 threads prueba que existen, eso alcanza para cambiar lo que es posible.