Mutexes in JavaScript

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple asynchronous operations.

class Container {
    constructor() {
        this.counters = { a: 0, b: 0 };
        this.mutex = new Mutex();
    }

    async inc(name) {
        await this.mutex.acquire();
        try {
            this.counters[name]++;
        } finally {
            this.mutex.release();
        }
    }
}

The Container class holds a map of counters. Since we want to update it concurrently from multiple asynchronous operations, we add a Mutex to synchronize access. Note that we’re using a third-party Mutex implementation as JavaScript doesn’t have built-in mutexes.

The inc method locks the mutex before accessing counters and unlocks it at the end of the function using a try-finally block to ensure the mutex is always released.

async function main() {
    const c = new Container();

    // This function increments a named counter in a loop
    const doIncrement = async (name, n) => {
        for (let i = 0; i < n; i++) {
            await c.inc(name);
        }
    };

    // Run several async operations concurrently
    const promises = [
        doIncrement("a", 10000),
        doIncrement("a", 10000),
        doIncrement("b", 10000)
    ];

    // Wait for all operations to finish
    await Promise.all(promises);

    console.log(c.counters);
}

main().catch(console.error);

In the main function, we create a Container instance and define an async function doIncrement that increments a named counter in a loop.

We then run several async operations concurrently using Promise.all. Note that they all access the same Container, and two of them access the same counter.

After all operations finish, we print the final state of the counters.

Running the program shows that the counters are updated as expected:

{ a: 20000, b: 10000 }

This example demonstrates how to use mutexes to safely manage shared state in concurrent JavaScript code. While JavaScript doesn’t have built-in support for true parallelism due to its single-threaded nature, this pattern is useful for managing concurrent access in asynchronous operations, such as when dealing with multiple API requests or other I/O operations.

Next, we’ll look at implementing this same state management task using only asynchronous functions and message passing.