Critical Section in Asynchronous Environment

Note: This article is automatically translated, please turn to the Chinese version for more accurate expression if possible.

Recently, I encountered an interesting problem when writing the check-in bot in the quiz group, so let’s talk about it.

After a punch-in dialogue is completed, the program needs to write the data into the file, and then it will call the NodeJS API (JavaScript):

class XXX {
  async saveData() {
    await fs.promises.writeFile(...)
    return
  }
}

Since the program is asynchronous and allows two people to check in at the same time, as a result, a check-in dialogue between two people ends at the same time, and the two dialogue functions are executed simultaneouslyawait saveData()

The consequence of this is that the firstwriteFile The operation returns normally. The person who executed the function first punched in and the function ended normally, and the second onewriteFile The operation never ends. the second writeFile The operation is called during the first operation, and there is no mutual exclusion, which may cause problems in the internal processing of NodeJS, becausewriteFile NodeJS will be responsible for the creation and recycling of file handles.

So, how to solve this problem? This is a typical code key area in lower-level languages, and we have to mutually exclusive the code that accesses file resources. In C++, mutual exclusion is achieved through semaphores. The first thread that accesses the resource is locked, and the threads that access later can only wait, and this waiting process is blocked, and the thread is suspended while waiting. However, in the JavaScript environment, most of the APIs provided by Node are non-blocking, and we cannot and should not solve this problem by calling blocking file writing functions.

The asynchronous nature makessaveData The function may be called at any time, and the same may be the last timesaveData Called before returning. In order to achieve saveData The internal code is mutually exclusive, we can use the queue to record allsaveData Call (that is, the Promise object returned by the flag function). In each call, first check whether there is an unfinished call in the queue. If there is, wait for the last function to execute before continuing to execute the key area logic. At the same time Put the Promise object you want to return into the queue before execution, and remove your Promise object from the queue after the execution ends. The implementation code is as follows:

let queue = []

async function serialized_async_function() {
  const promise = new Promise(async (resolve, reject) => {
    if (queue.length > 0)
      await queue[queue.length - 1]

    await fs.promises.writeFile(...) // any critical section logic

    resolve()
    queue.splice(0, 1)
  })
  queue.push(promise)
  return promise
}

Thanks to ES6’s promise object, it greatly simplifies the solution to this problem.