Mutexes in TypeScript

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.

import { Mutex } from 'async-mutex';

// Container holds a map of counters; since we want to
// update it concurrently from multiple async functions, we
// add a Mutex to synchronize access.
class Container {
    private mu: Mutex;
    private counters: Map<string, number>;

    constructor() {
        this.mu = new Mutex();
        this.counters = new Map<string, number>();
    }

    // Lock the mutex before accessing counters; unlock
    // it at the end of the function using the release function.
    async inc(name: string): Promise<void> {
        const release = await this.mu.acquire();
        try {
            const count = this.counters.get(name) || 0;
            this.counters.set(name, count + 1);
        } finally {
            release();
        }
    }

    getCounters(): Map<string, number> {
        return new Map(this.counters);
    }
}

async function main() {
    const c = new Container();
    c.counters.set('a', 0);
    c.counters.set('b', 0);

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

    // Run several async functions concurrently; note
    // that they all access the same Container,
    // and two of them access the same counter.
    const promises = [
        doIncrement('a', 10000),
        doIncrement('a', 10000),
        doIncrement('b', 10000)
    ];

    // Wait for all the async functions to finish
    await Promise.all(promises);
    console.log(c.getCounters());
}

main().catch(console.error);

Running the program shows that the counters updated as expected.

$ ts-node mutexes.ts
Map(2) { 'a' => 20000, 'b' => 10000 }

In this TypeScript version, we use the async-mutex package to implement mutex functionality. The Container class uses a Mutex to ensure thread-safe access to the counters map. The inc method is now asynchronous, using await to acquire and release the mutex.

The main function is also asynchronous, allowing us to use await with Promise.all to wait for all increments to complete. We use async functions instead of goroutines, and Promises instead of channels for concurrency.

Note that TypeScript runs on a single-threaded event loop by default, so true parallelism isn’t achieved here. However, this example demonstrates how to handle concurrent access to shared state in an asynchronous TypeScript application.

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