Race conditions in javascript

Understanding Javascript Async Race Conditions

The term «race condition» is usually applied to the conflict in accessing shared variables in a multi-threading environment. In Javascript, your JS code is executed only by a single thread at a time, but it’s still possible to create similar issues. This is a common problem when people are just blindly making their functions async, without thinking about the consequences. Let’s take a very simple example — lazy-loading some kind of single-instance resource. The synchronous version is simple:

let res; function get_resource()
let res; async function get_resource()

Imagine get_resource() being called in a web server, on every request. If enough time passes between the first and the second request, everything will work fine. But what happens if you get more requests, while the first one is still waiting for the resource? This can lead to serious problems that are very hard to debug.

More examples

 async function deduct(amt) < var balance = await getBalance(); if (balance >= amt) return await setBalance(balance - amt); > 
 async function totalSize(fol) < const files = await fol.getFiles(); let totalSize = 0; await Promise.all(files.map(async file =>< totalSize += await file.getSize(); >)); // totalSize is now way too small return totalSize; > 

Possible solutions

The best way to avoid these types of problems is to avoid async functions where it’s not absolutely necessary (see: Functional core, imperative shell). If it’s not possible, you may want to consider using Mutex (from Mutual Exclusion) — once someone acquires the lock, other requests will be blocked, until the original holder release the lock. i.e. with async-mutex package our previous example may look like this:

let res; async function get_resource() < await mutex.runExclusive(async () =>< if(!res) res = await init_resource(); >); return res; > 

async-mutex has also support for semaphores — it’s a similar concept, but multiple locks can be acquired. Note: this is a snapshot of (WIP) topic from the Understanding Simplicity wiki. All suggestions (and reactions) are welcome. You can find the latest version here: Understanding Javascript Async Race Conditions

Читайте также:  Php json decode try catch

Источник

Basics: Race condition

JavaScript does not support multithreading in the same way as the other programming languages do. However, there is a term race condition which means an unpleasant situation when the result of the program execution strongly depends on the order in which separate commands are performed.

Generally a race condition can occur when two or more threads are trying to get access to the same variable, at least one of them is willing to change a value of this variable and the threads don’t use any blocking. If these three conditions are met, the order of getting access to this variable becomes undefined which leads to various unexpected results.

The question whether JavaScript can have a race condition or not is still open to debate.

Arguments against the race condition in JavaScript

Since JavaScript is a single-threaded language one may think that a race condition is not possible. For example, two synchronous functions in the code below are trying to access the same variable. There should be no problem since they will be executed one by one. While one operation is being performed nothing else can happen, all other actions are blocked until the current one is finished.

bulb = "yellow"; syncToBlue(bulb); syncToGreen(bulb);

In some cases synchronized code may be found disadvantageous. That’s where asynchronous code (callbacks and promises) is used. Returning to the previous example with the light bulb you can use two asynchronous functions and then things will get a bit more complicated.

bulb = "yellow"; asyncToBlue(bulb); asyncToGreen(bulb);

Since you don’t know the time returned by any of these functions, you could say they are located in a heap. If you run these functions from the same network, because a syncToBlue() was run first, it will get to the queue faster. In case you run asyncToBlue() from a network with a lower speed, asyncToGreen() will get to the queue faster, so no race condition is possible in these situations.

However, if asyncToBlue() is running from a network with the speed of A and asyncToGreen() has a speed of B, you could say that the race condition can happen only if A is equal to B. This situation depends on the external circumstances (like the speed of the network) and not the program itself.

In reality the case like this is quite rare, but even if it does occur, the nature of JavaScript does not allow two functions to be executed at the same time. They will be placed in a queue instead and will be performed in specific order anyway.

Arguments for the race condition in JavaScript

Some situations prove that not only multithreading can be a reason for the race condition. Let’s create a program illustrating the possible existence of this issue. There will be a variable with the value of 0 and two functions trying to add 50 and 60 to it. To make things more interesting there will also be a promise for a random delay.

const randomDelay = () => new Promise(resolve => setTimeout(resolve, Math.random() * 100) )

In the following code the loadAmount() function is also added to simulate waiting for a response from the database. Another function — saveAmount() — will simulate the delay caused by writing some information into a database.

amount = 0; async function loadAmount () < await randomDelay(); return amount; > async function saveAmount (value) < await randomDelay(); return amount += value; >

Now let’s create a function for adding 50 to a variable. The two functions, loadAmount() and saveAmount() , will be awaited in order to create an imitation of the possible delay. We will also log the amount to the console after each command.

async function addFifty(value) < const amount = await loadAmount(); console.log ("Adding fifty, the current amount is " + amount); const newAmount = amount + 50; await saveAmount(newAmount); console.log("The amount after adding 50 is " + newAmount); >

The same concept can be applied to the function for adding 60 to the initial amount.

async function addSixty(value) < const amount = await loadAmount(); console.log ("Adding sixty, the current amount is " + amount); const newAmount = amount + 60; await saveAmount(newAmount); console.log("The amount after adding 60 is " + newAmount); >

Finally, let’s create the main function which will run all these processes. Note how there is no await command while calling the functions for adding, which allows transaction2 to be scheduled before transaction1 is done.

The await commands are implemented later to make sure both transactions are finished before loading of the final amount.

async function main () < const transaction1 = addFifty(); const transaction2 = addSixty(); await transaction1; await transaction2; const amount = await loadAmount(); console.log("Final amount is: " + amount); > main()

If you run the whole program several times you will get different results. For example, the current amount for both functions can be 0.

 // Possible outcome // Adding fifty, the current amount is 0 // Adding sixty, the current amount is 0 // The amount after adding 50 is 50 // The amount after adding 60 is 60 // Final amount is: 110

Of course, this example is a simple simulation, but it illustrates how the result of the program can be dependent on the order of operations execution. This is a proof that the race condition is possible in JavaScript even without multithreading.

To avoid risky outcomes you need to make sure there is no possibility for processes to be executed at the same time. You can rewrite the previous function to make it safe as shown below. Another approach to resolving this issue is the use of Mutex.

async function main () < await addFifty(); await addSixty(); const amount = await loadAmount() console.log("Final amount is: " + amount); >

Summary

Race condition is a problematic situation when several processes or threads are trying to have access to the same resource. Although some programmers insist that there is no race condition in JavaScript, you can still face some situations when resource sharing can lead to unexpected results. That’s why it’s very important to be careful with the flow of events in your program.

Источник

Борьба с гонками (race conditions) в JavaScript на примере работы с кешем

Рассмотрим следующую задачу. Нам необходимо делать вызовы стороннего API, которые считаются дорогими, и, следовательно, их необходимо кешировать в Redis. Мы используем современный NodeJS (версии 14+), а значит и конструкции async / await.

Напишем сначала класс обертку над вызовом API, где сам вызов будем эмулировать 2-секундным таймаутом.

class ApiWrapper < #callTimes = 0; async apiCall(payload) < return new Promise(resolve => < setTimeout(() =>< this.#callTimes++; resolve(`success: $`) >, 2000); >) > get callTimes() < return this.#callTimes; >> const run = async () => < const api = new ApiWrapper(); // эмулируем параллельный вызов API 4 раза const prDirect = await Promise.all([api.apiCall('test'), api.apiCall('test'), api.apiCall('test'), api.apiCall('test')]); console.log(prDirect); // =>['success: test', 'success: test', 'success: test', 'success: test'] console.log(apiCache.callTimes); // => 4 > run();

Я специально добавил в класс счетчик вызовов callTimes — он показывает сколько раз мы вызвали метод API. В данном примере у нас 4 прямых вызова.

Теперь добавим к коду кеширование в Redis. Для этого будем использовать пакет redis@next.

Код с кеширующим методом cachedApiCall

class ApiWrapper < #client; #callTimes = 0; constructor(url) < // создаем клиента Redis this.#client = createClient(< url >); > async init() < // Подключаемся к Redis await this.#client.connect(); >async apiCall(payload) < return new Promise(resolve => < setTimeout(() =>< this.#callTimes++; resolve(`success: $`) >, 2000); >) > async cachedApiCall(payload) < let data = await this.#client.get(payload); if (data === null) < // cache for 5 minutes data = await this.apiCall(payload); await this.#client.set(payload, data, < EX: 60 * 5 >); > return data; > get callTimes() < return this.#callTimes; >> const run = async () => < const api = new ApiWrapper('redis://10.250.200.9:6379/6'); await api.init(); const prCached = await Promise.all([api.cachedApiCall('test'), api.cachedApiCall('test'), api.cachedApiCall('test'), api.cachedApiCall('test')]); console.log(prCached); // =>['success: test', 'success: test', 'success: test', 'success: test'] console.log(api.callTimes); // => 4 >

Несмотря на то, что мы вызываем cachedApiCall, наш счетчик вызовов API всё равно показывает цифру 4. Это происходит из-за особенностей работы async / await.

Давайте детальней рассмотрим кеширующий метод. Я его написал так, как если бы я работал с синхронным кодом.

 async cachedApiCall(payload) < // получаем данные из кеша let data = await this.#client.get(payload); // если данных нет, то вызываем API и кладем в кеш if (data === null) < // cache for 5 minutes data = await this.apiCall(payload); await this.#client.set(payload, data, < EX: 60 * 5 >); > return data; >

В коде, при обращении к асинхронным методам, я использовал await. Следовательно, как только исполнение первого вызова cachedApiCall дойдет до этой строки оно прервется, и начнет работать следующий параллельный вызов (а у нас их 4). Так будет происходить на каждом вызове await. Если бы наше обращение к cachedApiCall не вызывалось параллельно, то проблемы бы в таком коде не было. Но при параллельном вызове мы нарвались на состояние гонки. Все 4 вызова соревнуются внутри метода, и в итоге мы имеем 4 запроса на получения значения кеша, 4 вызова API, и 4 вызова на установку значения кеша.

Как можно решить такую проблему? Нужно куда-то спрятать await, например во вложенной фукнции, а сам вызов вложенной функции кешировать в памяти на время работы основой функции.

Выглядеть это будет вот так:

 async cachedApiCall(payload) < // тут мы спраятали прошлый код во вложенную функцию getOrSet const getOrSet = async payload =>< let data = await this.#client.get(payload); if (data === null) < // cache for 5 minutes data = await this.apiCall(payload); await this.#client.set(payload, data, < EX: 60 * 5 >); > return data; > // во временном кеше на время работы функции мы храним промисы // если находясь тут у нас есть значение в tempCache, то мы словили // параллельный вызов if (typeof this.#tempCache[payload] !== 'undefined') return this.#tempCache[payload]; // конструкция try - finally нам позволяет почистить за собой при // любом исходе try < // помещаем во временный кеш промис this.#tempCache[payload] = getOrSet(payload); // используем await, чтобы все параллельные вызовы сюда зашли return await this.#tempCache[payload]; >finally < delete this.#tempCache[payload]; >>

Теперь, при параллельном вызове cachedApiCall все получат один и тот же промис, а значит вызовы к Redis и к нашему API произойдут всего 1 раз.

Боролись ли вы в своих JavaScript проектах с состоянием гонки и какие подходы вы применяли?

Источник

Оцените статью