1

I'm trying to build a Node application using worker threads, divided into three parts.

  • The primary thread that delegates tasks
  • A dedicated worker thread that updates shared data
  • A pool of worker threads that run calculations on shared data

The shared data is in the form of several SharedArrayBuffer objects operating like a pseudo-database. I would like to be able to update the data without needing to pause calculations, and I'm ok with a few tasks using slightly stale data. The flow I've come up with is:

  1. Primary thread passes data to update thread
  2. Update thread creates a whole new SharedArrayBuffer and populates it with updated data.
  3. Update thread returns a pointer to the new buffer back to primary thread.
  4. Primary thread caches the latest pointer in a variable, overwriting its previous value, and passes it to each worker thread with each task.
  5. Worker threads don't retain these pointers at all after executing their operations.

The problem is, this seems to create a memory leak in the resident state stack when I run a prototype that frequently makes updates and swaps out the shared buffers. Garbage collection appears to make a couple of passes removing the discarded buffers, but then it climbs continuously until the application slows and eventually hangs or crashes.

How can I guarantee that a SharedArrayBuffer will get picked up by garbage collection when I'm done with it, or it it even possible? I've seen hints to the effect that as long as all references to it are removed from all threads it will eventually get picked up, but not a clear answer.

I'm using the threads.js library to abstract the worker thread operations. Here's a summary of my prototype:

app.ts:

import { ModuleThread, Pool, spawn, Worker } from "threads";
import { WriterModule } from "./workers/writer-worker";
import { CalculateModule } from "./workers/calculate-worker";

class App {
    calculatePool = Pool<ModuleThread<CalculateModule>>
        (() => spawn(new Worker('./workers/calculate-worker')), { size: 6 });
    writerThread: ModuleThread<WriterModule>;

    sharedBuffer: SharedArrayBuffer;
    dataView: DataView;

    constructor() {
        this.sharedBuffer = new SharedArrayBuffer(1000000);
        this.dataView = new DataView(this.sharedBuffer);
    }

    async start(): Promise<void> {
        this.writerThread = await spawn<WriterModule>(new Worker('./workers/writer-worker'));
        await this.writerThread.init(this.sharedBuffer);

        await this.update();

        // Arbitrary delay between updates
        setInterval(() => this.update(), 5000);

        while (true) {
            // Arbitrary delay between tasks
            await new Promise<void>(resolve => setTimeout(() => resolve(), 250));

            this.calculate();
        }
    }

    async update(): Promise<void> {
        const updates: any[] = [];
        
        // generates updates

        this.sharedBuffer = await this.writerThread.update(updates);
        this.dataView = new DataView(this.sharedBuffer);
    }

    async calculate(): Promise<void> {
        const task = this.calculatePool.queue(async (calc) => calc.calculate(this.sharedBuffer));
        const sum: number = await task;
        // Use result
    }
}

const app = new App();
app.start();

writer-worker.ts:

import { expose } from "threads";

let sharedBuffer: SharedArrayBuffer;

const writerModule = {
    async init(startingBuffer: SharedArrayBuffer): Promise<void> {
        sharedBuffer = startingBuffer;
    },

    async update(data: any[]): Promise<SharedArrayBuffer> {
        // Arbitrary update time
        await new Promise<void>(resolve => setTimeout(() => resolve(), 500));

        const newSharedBuffer = new SharedArrayBuffer(1000000);

        // Copy some values from the old buffer over, perform some mutations, etc.

        sharedBuffer = newSharedBuffer;

        return sharedBuffer;
    },
}

export type WriterModule = typeof writerModule;
expose(writerModule);

calculate-worker.ts

import { expose } from "threads";

const calculateModule = {
    async calculate(sharedBuffer: SharedArrayBuffer): Promise<number> {
        const view = new DataView(sharedBuffer);

        // Arbitrary calculation time
        await new Promise<void>(resolve => setTimeout(() => resolve(), 100));

        // Run arbitrary calculation
        
        return sum;
    }
}

export type CalculateModule = typeof calculateModule;

expose(calculateModule);
Eric N
  • 218
  • 3
  • 13

0 Answers0