Понимание условий асинхронной гонки Javascript
Термин «состояние гонки» обычно применяется к конфликту при доступе к общим переменным в многопоточной среде. В Javascript ваш JS-код выполняется только одним потоком за раз, но все же можно создавать похожие проблемы.
Это распространенная проблема, когда люди просто вслепую делают свои функции асинхронными, не задумываясь о последствиях.
Возьмем очень простой пример — ленивая загрузка какого-то ресурса с одним экземпляром.
Синхронная версия проста:
let res;
function get_resource() {
if(!res) res = init_resource();
return res;
}
Асинхронная версия:
let res;
async function get_resource() {
if(!res) res = await init_resource();
return res;
}
Представьте get_resource()
, что вас вызывают на веб-сервер при каждом запросе. Если между первым и вторым запросом проходит достаточно времени, все будет работать нормально. Но что произойдет, если вы получите больше запросов, в то время как первый все еще ждет ресурс?
Это может привести к серьезным проблемам, которые очень трудно устранить.
Другие примеры
Вот несколько примеров (из этой ветки HN):
Баланс:
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;
}
Возможные решения
Лучший способ избежать подобных проблем — избегать асинхронных функций там, где это не является абсолютно необходимым (см.: [[Функциональное ядро, императивная оболочка]]).
Если это невозможно, вы можете рассмотреть возможность использования Mutex (из взаимного исключения) — как только кто-то получит блокировку, другие запросы будут заблокированы до тех пор, пока первоначальный держатель не освободит блокировку.
Т.е. с пакетом async-mutex наш предыдущий пример может выглядеть так:
let res;
async function get_resource() {
await mutex.runExclusive(async () => {
if(!res) res = await init_resource();
});
return res;
}
async-mutex
также поддерживает семафоры - это аналогичная концепция, но можно получить несколько блокировок.