Atomic Counters in JavaScript

Our primary mechanism for managing state in JavaScript is typically through the use of variables and objects. However, when dealing with concurrent operations, we need to be careful about how we modify shared state. In this example, we’ll look at using atomic operations for managing a counter accessed by multiple asynchronous functions.

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
    // We'll use a SharedArrayBuffer to store our counter
    const sharedBuffer = new SharedArrayBuffer(4);
    const counter = new Uint32Array(sharedBuffer);

    // We'll start 50 workers that each increment the counter exactly 1000 times
    const workerCount = 50;
    let completedWorkers = 0;

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

        worker.on('message', () => {
            completedWorkers++;
            if (completedWorkers === workerCount) {
                // All workers have finished, print the final count
                console.log('ops:', Atomics.load(counter, 0));
            }
        });
    }
} else {
    // This code runs in the worker threads
    const sharedBuffer = workerData;
    const counter = new Uint32Array(sharedBuffer);

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

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

In this JavaScript version, we’re using the worker_threads module to create multiple threads, similar to goroutines in the original example. We use a SharedArrayBuffer and Atomics to safely manage our shared counter across these threads.

To run this program, save it as atomic-counters.js and use Node.js to execute it:

$ node atomic-counters.js
ops: 50000

We expect to get exactly 50,000 operations. If we had used a regular variable and incremented it with counter++, we’d likely get a different number, changing between runs, because the workers would interfere with each other.

It’s worth noting that JavaScript’s concurrency model is different from many other languages. It typically uses an event loop and asynchronous operations rather than true parallel execution. The worker_threads module provides a way to use multiple CPU cores for parallel execution in Node.js, which is why we’re using it in this example to simulate the behavior of the original Go code.

Next, we’ll look at other synchronization primitives available in JavaScript for managing state in concurrent operations.