Atomic Counters in TypeScript

Here’s the translation of the atomic counters example from Go to TypeScript, with explanations adapted for TypeScript:

import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';

// In TypeScript, we'll use a SharedArrayBuffer to represent our
// atomic counter, as it can be shared between threads.
const sharedBuffer = new SharedArrayBuffer(4);
const counter = new Uint32Array(sharedBuffer);

if (isMainThread) {
    // This is the main thread code

    // We'll use 50 worker threads to increment the counter
    const numWorkers = 50;
    const incrementsPerWorker = 1000;

    let completedWorkers = 0;

    for (let i = 0; i < numWorkers; i++) {
        const worker = new Worker(__filename, {
            workerData: { sharedBuffer, incrementsPerWorker }
        });

        worker.on('message', () => {
            completedWorkers++;
            if (completedWorkers === numWorkers) {
                // All workers have finished
                console.log('ops:', Atomics.load(counter, 0));
            }
        });
    }
} else {
    // This is the worker thread code
    const { sharedBuffer, incrementsPerWorker } = workerData;
    const counter = new Uint32Array(sharedBuffer);

    for (let i = 0; i < incrementsPerWorker; i++) {
        // To atomically increment the counter we use Atomics.add
        Atomics.add(counter, 0, 1);
    }

    // Signal that this worker has completed its work
    parentPort?.postMessage('done');
}

This TypeScript code demonstrates the use of atomic operations in a multi-threaded environment. Here’s a breakdown of what’s happening:

  1. We import necessary modules from Node.js’s worker_threads to handle multi-threading.

  2. We create a SharedArrayBuffer and a Uint32Array view of it to represent our atomic counter. This allows the counter to be shared between threads.

  3. In the main thread:

    • We set up 50 worker threads, each of which will increment the counter 1000 times.
    • We create and start each worker, passing the shared buffer and the number of increments to perform.
    • We set up a message listener for each worker to track when they’ve completed their work.
  4. In the worker threads:

    • Each worker receives the shared buffer and the number of increments to perform.
    • It uses Atomics.add to safely increment the counter.
    • After completing its work, it sends a message back to the main thread.
  5. Back in the main thread, once all workers have reported completion, we use Atomics.load to safely read the final value of the counter.

This approach ensures that all increments to the counter are atomic, preventing race conditions that could occur with non-atomic operations in a multi-threaded environment.

To run this program:

$ ts-node atomic-counters.ts
ops: 50000

We expect to get exactly 50,000 operations. The use of atomic operations ensures that we get the correct count, even with multiple threads concurrently incrementing the counter.

Next, we could explore other synchronization primitives in TypeScript and Node.js, such as Mutexes or Semaphores, which can be implemented using libraries like async-mutex.