Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-Threading

Perry gives you real OS threads with a one-line API. No worker setup, no message ports, no structured clone overhead. Just parallelMap, parallelFilter, and spawn.

async function overviewHeader(): Promise<void> {
    const data = [1, 2, 3, 4, 5, 6, 7, 8]
    const records = [
        { score: 50, id: 1 },
        { score: 90, id: 2 },
        { score: 85, id: 3 },
    ]
    const threshold = 80

    // Process a million items across all CPU cores
    const results = parallelMap(data, (item: number) => item * item)

    // Filter a large dataset in parallel
    const valid = parallelFilter(records, (r: { score: number; id: number }) => r.score > threshold)

    // Run expensive work in the background
    const answer = await spawn(() => {
        let acc = 0
        for (let i = 0; i < 100_000; i++) acc += i
        return acc
    })

    console.log(`overview-header results=${results.length} valid=${valid.length} answer=${answer}`)
}

This is something no JavaScript runtime can do. V8, Bun, and Deno are all locked to one thread per isolate. Perry compiles to native code — there are no isolates, no GIL, no structural limitations. Your code runs on real OS threads with the full power of every CPU core.

Why This Matters

JavaScript’s single-threaded model is its biggest performance bottleneck. Here’s how runtimes try to work around it:

Runtime“Multi-threading”Reality
Node.jsworker_threadsSeparate V8 isolates. Data copied via structured clone. ~2MB RAM per worker. Complex API.
DenoWorkerSame as Node — isolated heaps, message passing only.
BunWorkerSame architecture. Faster structured clone, still isolated.
PerryparallelMap / spawnReal OS threads. Lightweight (8MB stack). One-line API. Compile-time safety.

The fundamental problem: V8 uses a garbage-collected heap that cannot be shared between threads. Every “worker” is an entirely separate JavaScript engine instance with its own heap, its own GC, and its own copy of your data.

Perry doesn’t have this limitation. It compiles TypeScript to native machine code. Values are transferred between threads using zero-cost copies for numbers and efficient serialization for objects — no separate engine instances, no multi-megabyte overhead per thread.

Three Primitives

parallelMap — Data-Parallel Processing

Split an array across all CPU cores. Each element is processed independently. Results are collected in order.

async function overviewParallelMap(): Promise<void> {
    const prices = [100, 200, 300, 400, 500, 600, 700, 800]
    const adjusted = parallelMap(prices, (price: number) => {
        // Heavy computation runs on a worker thread
        let result = price
        for (let i = 0; i < 1000000; i++) {
            result = Math.sqrt(result * result + i)
        }
        return result
    })

    console.log(`overview-parallel-map len=${adjusted.length}`)
}

Perry automatically:

  1. Detects the number of CPU cores
  2. Splits the array into chunks (one per core)
  3. Spawns OS threads to process each chunk
  4. Collects results in the original order
  5. Returns a new array

For small arrays, Perry skips threading entirely and processes inline — no overhead for trivial cases.

parallelFilter — Data-Parallel Filtering

Filter a large array across all CPU cores. Like .filter() but parallel:

async function overviewParallelFilter(): Promise<void> {
    const cutoffDate = 1_700_000_000
    const users = [
        { lastLogin: 1_710_000_000, score: 150, name: "alice" },
        { lastLogin: 1_690_000_000, score: 50, name: "bob" },
        { lastLogin: 1_720_000_000, score: 120, name: "carol" },
    ]

    // Filter across all cores — order is preserved
    const active = parallelFilter(users, (user: { lastLogin: number; score: number; name: string }) => {
        return user.lastLogin > cutoffDate && user.score > 100
    })

    console.log(`overview-parallel-filter active=${active.length}`)
}

Same rules as parallelMap: closures cannot capture mutable variables (compile-time enforced), and values are deep-copied between threads.

spawn — Background Threads

Run any computation in the background and get a Promise back. The main thread continues immediately.

async function overviewSpawnBg(): Promise<void> {
    // Start heavy work in the background
    const handle = spawn(() => {
        let sum = 0
        for (let i = 0; i < 100_000; i++) {
            sum += Math.sin(i)
        }
        return sum
    })

    // Main thread keeps running — UI stays responsive
    console.log("Computing...")

    // Get the result when you need it
    const result = await handle
    console.log(`Done: len=${typeof result}`)
}

spawn returns a standard Promise. You can await it, pass it to Promise.all, or chain .then() — it works exactly like any other async operation.

Practical Examples

Parallel Image Processing

async function overviewImage(): Promise<void> {
    const pixels = [
        { r: 100, g: 120, b: 140 },
        { r: 50, g: 60, b: 70 },
        { r: 200, g: 210, b: 220 },
    ]

    // Each pixel processed on a separate core
    const processed = parallelMap(pixels, (pixel: { r: number; g: number; b: number }) => {
        const r = Math.min(255, pixel.r * 1.2)
        const g = Math.min(255, pixel.g * 0.8)
        const b = Math.min(255, pixel.b * 1.1)
        return { r, g, b }
    })

    console.log(`overview-image processed=${processed.length}`)
}

Parallel Cryptographic Hashing

async function overviewCrypto(): Promise<void> {
    // Hash thousands of items across all cores
    const passwords = ["pass1", "pass2", "pass3"]
    const hashed = parallelMap(passwords, (password: string) => {
        // Stand-in for a real hash: deterministic FNV-1a over the bytes.
        let h = 2166136261
        for (let i = 0; i < password.length; i++) {
            h ^= password.charCodeAt(i)
            h = (h * 16777619) >>> 0
        }
        return h
    })

    console.log(`overview-crypto hashed=${hashed.length}`)
}

Multiple Independent Computations

async function overviewMultiple(): Promise<void> {
    const dataA = [1, 2, 3]
    const dataB = [4, 5, 6]
    const dataC = [7, 8, 9]

    // Three independent tasks run simultaneously on three OS threads
    const task1 = spawn(() => {
        let acc = 0
        for (const v of dataA) acc += v * v
        return acc
    })
    const task2 = spawn(() => {
        let acc = 0
        for (const v of dataB) acc += v * v
        return acc
    })
    const task3 = spawn(() => {
        let acc = 0
        for (const v of dataC) acc += v * v
        return acc
    })

    // All three run concurrently
    const [result1, result2, result3] = await Promise.all([task1, task2, task3])
    console.log(`overview-multiple ${result1} ${result2} ${result3}`)
}

Keeping UI Responsive

const responsiveButton = Button("Start Analysis", async () => {
    status.set("Analyzing...")

    // Heavy computation runs on a background thread
    // UI stays responsive — user can still interact
    const value = await spawn(() => {
        let acc = 0
        for (let i = 0; i < 1_000_000; i++) acc += i
        return acc
    })

    status.set(`Done: ${value}`)
})

const responsiveText = Text(`Status: ${status.value}`)

Captured Variables

Closures can capture outer variables. Captured values are automatically deep-copied to each worker thread:

async function overviewCaptured(): Promise<void> {
    const prices = [100, 200, 300, 400]
    const taxRate = 0.08
    const discount = 0.15

    // taxRate and discount are captured and copied to each thread
    const finalPrices = parallelMap(prices, (price: number) => {
        const discounted = price * (1 - discount)
        return discounted * (1 + taxRate)
    })

    console.log(`overview-captured len=${finalPrices.length}`)
}

Numbers and booleans are zero-cost copies (just 64-bit values). Strings, arrays, and objects are deep-copied automatically.

Safety

Perry enforces thread safety at compile time. You don’t need to think about race conditions, mutexes, or data corruption.

No Shared Mutable State

Closures passed to parallelMap and spawn cannot capture mutable variables. The compiler rejects this:

// Reject example — Perry rejects this at compile time:

let counter = 0;

// COMPILE ERROR: Closures passed to parallelMap cannot
// capture mutable variable 'counter'
parallelMap(data, (item) => {
    counter++;  // Not allowed
    return item;
});

This eliminates data races by design. If you need to aggregate results, use the return values:

async function overviewReduceInstead(): Promise<void> {
    const data = [1, 2, 3, 4, 5, 6, 7, 8]

    // Instead of mutating a shared counter, return values and reduce
    const results = parallelMap(data, (item: number) => item * item)
    const total = results.reduce((sum: number, r: number) => sum + r, 0)

    console.log(`overview-reduce-instead total=${total}`)
}

Independent Thread Arenas

Each worker thread has its own memory arena. Objects created on one thread can never be accessed from another thread. Values cross thread boundaries only through deep-copy serialization, which Perry handles automatically and invisibly.

How It Works

Perry’s threading model is built on three pillars:

1. Native Code, Not Interpreted

Perry compiles TypeScript to native machine code via LLVM. There’s no interpreter, no VM, no isolate. A function pointer is just a function pointer — it’s valid on any thread.

2. Thread-Local Memory

Each thread gets its own memory arena (bump allocator) and garbage collector. No synchronization overhead during computation. When a thread finishes, its arena is freed automatically.

3. Serialized Transfer

Values crossing thread boundaries are serialized to a thread-safe intermediate format and deserialized on the target thread. The cost depends on the value type:

Value TypeTransfer Cost
Numbers, booleans, null, undefinedZero-cost (64-bit copy)
StringsO(n) byte copy
ArraysO(n) deep copy of elements
ObjectsO(n) deep copy of fields
ClosuresPointer + captured values

For numeric workloads — the most common parallelizable tasks — the threading overhead is negligible.

Next Steps