multithreading.js

repository·main·Indexed 23 days ago

https://github.com/w4g1/multithreading

A TypeScript library providing Rust-inspired concurrency primitives for JavaScript. It abstracts WebWorkers and SharedArrayBuffer to enable safe, high-performance multi-threading via a managed worker pool. Features include synchronization primitives (Mutex, RwLock, Semaphore, Condvar, Barrier), MPMC bounded channels, SharedJsonBuffer for shared memory, and move semantics for explicit ownership transfer.

Tokens
7.9K
Snippets
17
Records
50
Agent score
81%

What's inside multithreading.js

  1. How multithreading.js works

    main

    Multithreading.js abstracts the complexities of WebWorkers, SharedArrayBuffer, and serialization. It provides a managed thread-pool architecture that automatically scales based on hardware concurrency. Key features include:

    • Managed Worker Pool: Automatic thread management.
    • Shared Memory Primitives: Safe state sharing without race conditions.
    • Scoped Imports: Ability to import modules/files directly within worker tasks.
    • Move Semantics: Explicit ownership transfer to minimize cloning overhead.
  2. Use MPMC Channels for worker communication

    main

    The library provides a Multi-Producer, Multi-Consumer (MPMC) bounded channel that acts as a work-stealing queue. It handles backpressure (blocking send() when full) and load-balances messages across receivers (blocking recv() when empty).

    Key characteristics:

    • Exactly-once delivery: Each message is delivered to exactly one consumer.
    • JSON Support: Uses SharedJsonBuffer to allow sending any JSON-serializable value (objects, arrays, etc.).
    • Bounded: You define a capacity.
    • Clonable: Sender and Receiver can be cloned and moved to different workers.
    • Reference Counted: The channel closes automatically when all Sender handles or all Receiver handles are dropped.
    import { spawn, move, channel } from "multithreading";
    
    const [tx, rx] = channel();
    
    // Producer
    spawn(move(tx), async (sender) => {
      await sender.send({ hello: "world" });
      await sender.send({ hello: "multithreading" });
    });
    
    // Consumer
    await spawn(move(rx.clone()), async (receiver) => {
      const result = await receiver.recv();
      console.log("Worker got:", result.value); // { hello: "world" }
    }).join();
    
    // Because we cloned rx, the main thread also still has a rx handle
    for await (const value of rx) {
      console.log("Main thread got:", value); // { hello: "multithreading" }
    }
  3. Quick Start: Spawning tasks with `spawn()`

    main

    The spawn function is the primary entry point. It submits a task to the thread pool and returns a handle. Use handle.join() to await the result of the background task.

    import { spawn } from "multithreading";
    
    // Spawn a task on a background thread
    const handle = spawn(() => {
      // This code runs in a separate worker
      return Math.random();
    });
    
    // Wait for the result
    const result = await handle.join(); // { ok: true, value: 0.6378467071314606 }
  4. Import modules and relative files inside Workers

    main

    The library supports standard dynamic await import() calls inside workers. It automatically handles path resolution so that imports are relative to the file that called spawn, rather than the worker's internal location.

    You can import:

    1. External Libraries: npm packages or CDNs.
    2. Relative Files: Local modules relative to the caller.

    Note: The function passed to spawn must be self-contained or explicitly import its dependencies. It cannot access variables from the outer scope unless they are passed via move().

    // main.ts
    import { spawn } from "multithreading";
    
    spawn(async () => {
      // Importing relative files
      const utils = await import("./utils.ts");
      // Importing external libraries
      const { v4: uuiv4 } = await import("uuid");
    
      console.log("Magic number from relative file:", utils.magicNumber);
      console.log("Random UUID:", uuiv4());
      
      return utils.magicNumber;
    });
  5. Configure Browser requirements for Synchronization Primitives

    main

    While core features like spawn and move work in modern browsers by default, synchronization primitives (Mutex, RwLock, SharedJsonBuffer, etc.) require Cross-Origin Isolation because they rely on SharedArrayBuffer.

    Your server must send these headers:

    Cross-Origin-Opener-Policy: same-origin
    Cross-Origin-Embedder-Policy: require-corp
  6. Configure Content Security Policy (CSP) for Workers

    main

    The library uses dynamic imports via data: and blob: URLs to generate worker entry points. If your application uses a CSP, you must allow these schemes in your directives.

    Required CSP configuration:

    • script-src must allow data:
    • worker-src must allow blob:

    Example header:

    Content-Security-Policy: default-src 'self'; worker-src 'self' blob:; script-src 'self' data: https:;
  7. Use RwLock for read-heavy shared memory access

    main

    The RwLock (Read-Write Lock) allows multiple threads to hold a read lock simultaneously, but only one thread can hold a write lock. This is ideal for data structures that are read frequently but updated infrequently.

    To use it, you can acquire either a read lock or a write lock. The lock provides a "Guard" object that grants access to the underlying data. You must release the guard to unlock the resource. The guards implement the Disposable interface, so the recommended pattern is to use the using keyword (or call .dispose()) to ensure the lock is released even if an error occurs.

  8. Manage mutual exclusion with `Mutex`

    main

    A Mutex ensures only one thread can access specific data at a time.

    Use the using keyword (Explicit Resource Management) to automatically release the lock when the guard goes out of scope.

    Manual Management (Bun / Standard JS)

    If using Bun (where using may fail in worker contexts) or standard JS, you must manually call .dispose() on the guard. Always use a try...finally block to ensure the lock is released.

    Best Practice: Use asynchronous methods (lock()) to avoid halting the entire Worker thread.

    import { spawn, move, Mutex } from "multithreading";
    
    // Option A: Automatic Management
    const buffer = new SharedArrayBuffer(4);
    const counterMutex = new Mutex(new Int32Array(buffer));
    
    spawn(move(counterMutex), async (mutex) => {
      using guard = await mutex.lock();
      guard.value[0]++;
    });
    
    // Option B: Manual Management (Required for Bun)
    const counterMutexManual = new Mutex(new Int32Array(new SharedArrayBuffer(4)));
    
    spawn(move(counterMutexManual), async (mutex) => {
      const guard = await mutex.lock();
      try {
        guard.value[0]++;
      } finally {
        guard.dispose();
      }
    });
  9. Using `SharedJsonBuffer` for complex objects

    main

    SharedJsonBuffer allows for Mutex-protected shared memory for JSON objects. It is optimized for high-performance state synchronization of large, persistent objects by using Proxies to reserialize only changed bytes rather than the entire tree.

    Note: SharedJsonBuffer has an initialization cost. It is best used for frequent incremental updates to large objects, rather than single-use transfers where standard cloning is faster.

    import { spawn, move, Mutex, SharedJsonBuffer } from "multithreading";
    
    const sharedState = new Mutex(new SharedJsonBuffer({
      score: 0,
      players: ["Main Thread"],
      level: {
        id: 1,
        title: "Start",
      },
    }));
    
    await spawn(move(sharedState), async (sharedState) => {
      using guard = await sharedState.lock();
    
      const state = guard.value;
    
      console.log(`Current Score: ${state.score}`);
    
      // Modify the data
      state.score += 100;
      state.players.push("Worker1");
    
      // End of scope: Lock is automatically released here
    }).join();
    
    // Verify on main thread
    using guard = await sharedState.lock();
    
    console.log(guard.value); // { score: 100, players: ["Main Thread", "Worker1"], ... }
  10. Wait for conditions with `Condvar`

    main

    A Condvar (Condition Variable) allows threads to sleep until a specific condition is met, saving CPU resources. Use cv.wait(guard) to unlock the mutex, wait for a notification, and then re-acquire the mutex.

    import { spawn, move, Mutex, Condvar } from "multithreading";
    
    const mutex = new Mutex(new Int32Array(new SharedArrayBuffer(4)));
    const cv = new Condvar();
    
    spawn(move(mutex, cv), async (mutex, cv) => {
      using guard = await mutex.lock();
      
      while (guard.value[0] === 0) {
        await cv.wait(guard);
      }
      
      console.log("Received signal, value is:", guard.value[0]);
    });
  11. Optimize read-heavy workloads with `RwLock`

    main

    A RwLock (Read-Write Lock) allows multiple simultaneous readers but only one writer. This is ideal for data that is read frequently but updated rarely.

    import { spawn, move, RwLock } from "multithreading";
    
    const lock = new RwLock(new Int32Array(new SharedArrayBuffer(4)));
    
    // Spawning a Writer
    spawn(move(lock), async (l) => {
      using guard = await l.write(); 
      guard.value[0] = 42;
    });
    
    // Spawning Readers
    spawn(move(lock), async (l) => {
      using guard = await l.read(); 
      console.log(guard.value[0]);
    });