Async-Mutex: A JavaScript Library to Prevent Race Conditions

·

Race condition is where the system’s substantive behavior is dependent on the sequence or timing of other uncontrollable events, leading to unexpected or inconsistent results. Every engineer soon or later will face this situation, it’s so frustating when a data handled by more than 1 operation which lead unexpected or inconsistent results. This situation sometimes easy to find, but sometimes hard to identify because application have tons of operation in the same times.

We will face this situation in different scenario, for example:

  • Backend server receive more than 1 request run parallely to update a row database.
  • Application run more than 1 thread update the same value
  • Frontend javascript application run asyncronously to update token

We should aware this situation or it can lead unexpected result also need to handle carefully to make it work a expected

Identification

We need to understand what cause this situation.

  1. Single source of data.
  2. Read by more than one process
  3. Write by asyncronously or parallely

Multiple process write data will lead unexpected result because we don’t know who use it. We need to create mechanism to manage “who able to write or read”, in other word we need to “lock” it as long as a process use it and “release” when finish.

Example race condition in javascript

let sharedValue = 0;

function incrementSharedValue() {
  const temp = sharedValue; // Read the value
  return new Promise((resolve) => {
    setTimeout(() => {
      sharedValue = temp + 1; // Write the incremented value
      console.log(`Incremented value: ${sharedValue}`);
      resolve();
    }, Math.random() * 100);
  })
}

(async () => {
  await Promise.all([
    incrementSharedValue(),
    incrementSharedValue(),
    incrementSharedValue(),
  ]);
})()

// Expected: 1, 2, 3 (sequential)
// Actual: Could be out of order or have incorrect increments due to race condition.

And the return is

 node main.js
Incremented value: 1
Incremented value: 1
Incremented value: 1

Each function run asyncronously “read” and “write” to shared value then lead incorrect increments. Race condition happen when function read and write

Lock Mechanism

Mutex (Mutual Exclusion) is a data structure asyncronous primitive to prevents state from being modified or accessed by multiple thread. This scenario usually happen when using multiple thread, most of the programming language that support multi thread always include mutex, for example Rust Mutex, c# mutex, Golang Mutex.

The concept of mutex is:

  1. A thread request access and mutex will return is it free or locked.
  2. If locked, the thread will wait until it free
  3. If free, the thread will lock the mutex and update the data freely
  4. Thread will release the mutex after finish update the value.

Async-mutex: Javascript Mutex Library

Unfortunately, javascript is single thread programming language and there is no mutex or lock mechanism built-in. But, there is third-party package async-mutex to create locking mechanism asyncronous of javascript environment and it support run in browser and nodejs. Simply install async-mutex using npm.

ShellScript
$ npm install async-mutex

Solve previous code by using async-mutex

const { Mutex } = require('async-mutex');
const mutex = new Mutex();

let sharedValue = 0;

function incrementSharedValue() {
  return new Promise(async (resolve) => {
    const release = await mutex.acquire(); // Acquire the lock
    const temp = sharedValue; // Read the value
    setTimeout(() => {
      sharedValue = temp + 1; // Write the incremented value
      console.log(`Incremented value: ${sharedValue}`);
      resolve();
      release(); // Release the lock
    }, Math.random() * 100);
  })
}

(async () => {
  await Promise.all([
    incrementSharedValue(),
    incrementSharedValue(),
    incrementSharedValue(),
  ]);
})()

// Expected: 1, 2, 3 (sequential)
 node lock-mechanism.js
Incremented value: 1
Incremented value: 2
Incremented value: 3

I will explain what we do here

  • line 8: We acquire the lock, so other process will wait until it’s released
  • line 9-12: read value and write value
  • line 14: release the lock

Locking mechanism require async to prevent race condition, waiting when it’s locked and lock the resource if it’s free.

Also you can use runExclusive to wrap your logic system. It will automatically lock and release when async process is done.

JavaScript
const { Mutex } = require('async-mutex');
const mutex = new Mutex();

let sharedValue = 0;

function incrementSharedValue() {
  return mutex.runExclusive(async () => {
    const temp = sharedValue; // Read the value
    await new Promise((resolve) => {
      setTimeout(() => {
        sharedValue = temp + 1; // Write the incremented value
        console.log(`Incremented value: ${sharedValue}`);
        resolve();
      }, Math.random() * 100);
    })
  });
}

(async () => {
  await Promise.all([
    incrementSharedValue(),
    incrementSharedValue(),
    incrementSharedValue(),
  ]);
})()

// Expected: 1, 2, 3 (sequential)

It’s easy to use this library, you don’t need to manually lock or release mutex (although you can if needed). Calling an async process usually requires await to make it execute synchronously.

Even this looks like syncronously, but actually all processes are running asyncronously and each process “wait” the mutex unlocked before doing read-write shared data.

This library also provide more function to handle mutex like waitForUnlock and semaphore. We will talk about semaphore later. Read more about this library in their full-documentation.

Conclusion

Race condition is common problem in software engineering and we need to handled carefully. Locking mechanism is the one solution for this scenario, thanks to mutex. But, javascript have no built-in mutex, so we need to install library mutex-aysnc for it. Hopefully this can help you in your scenario, thank you for your reading, see you next time.

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Discover more from Rio Chandra Notes

    Subscribe now to keep reading and get access to the full archive.

    Continue reading