Я знаю, что JavaScript является однопоточным и технически не может иметь условий гонки, но предположительно может иметь некоторую неопределенность из-за асинхронности и цикла событий. Вот упрощенный пример:
class TestClass {
// ...
async a(returnsValue) {
this.value = await returnsValue()
}
b() {
this.value.mutatingMethod()
return this.value
}
async c(val) {
await this.a(val)
// do more stuff
await otherFunction(this.b())
}
}
Предположим, что b() полагается на то, что this.value не изменилось с момента вызова a(), а c(val) вызывается много раз в быстрой последовательности из разных мест программы. Может ли это привести к гонке данных, когда this.value меняется между вызовами a() и b()?
Для справки, я предварительно исправил свою проблему с мьютекс, но я задавался вопросом, была ли проблема с самого начала.
Кроме того, очень легко написать собственный «мьютекс» для принудительного взаимного исключения асинхронных контекстов. Если вам интересно, я могу предоставить ответ, содержащий пример реализации и демонстрацию.
Условия гонки - это параллелизм, а не параллелизм. Как вы заметили, JavaScript делает имеет параллелизм в форме async/await, где вы можете чередовать несколько «логических» потоков. В JavaScript отсутствует параллелизм (т. е. наличие нескольких потоков выполнения, работающих в один и тот же момент). stackoverflow.com/questions/1050222/…
@Bergi Вы правы ... хотя я думаю, что это было достаточно ясно для меня, я изменил пример, сделав его «настоящей» асинхронной функцией, чтобы сделать его более понятным для будущих читателей.
Ах, это хотя бы один - да, состояние гонки все еще есть, но это очень редко, так как вам нужно точно определить время микрозадачи. В частности, this.value может быть заменен какой-либо другой микрозадачей во время await перед this.a(val), в противном случае .value выглядит так, как будто this.value = await val; this.b() используется «сразу» после его назначения. (Обратите внимание, что не было бы проблемы, если бы вы написали a в том же методе). Состояние гонки было бы более очевидным, если бы this.value = returnsValue(); await delay(1000) сделал this.value



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Да, условия гонки могут возникать и возникают и в JS. Тот факт, что он однопоточный, не означает, что условия гонки не могут возникнуть (хотя они встречаются реже). JavaScript действительно является однопоточным, но также и асинхронным: логическая последовательность инструкций часто делится на более мелкие фрагменты, выполняемые в разное время. Это делает возможным чередование и, следовательно, возникают условия гонки.
Для простого примера рассмотрим...
var x = 1;
async function foo() {
var y = x;
await delay(100); // whatever async here
x = y+1;
}
... который является классическим примером неатомарного приращения, адаптированного к асинхронному миру JavaScript.
Теперь сравните следующее «параллельное» выполнение:
await Promise.all([foo(), foo(), foo()]);
console.info(x); // prints 2
... с "последовательным":
await foo();
await foo();
await foo();
console.info(x); // prints 4
Обратите внимание, что результаты разные, т. Е. foo() не является «асинхронным безопасным».
Даже в JS иногда приходится использовать «асинхронные мьютексы». И ваш пример может быть одной из таких ситуаций, в зависимости от того, что происходит между ними (например, если происходит какой-то асинхронный вызов). Без асинхронного вызова в do more stuff похоже, что мутация происходит в одном блоке кода (ограниченном асинхронными вызовами, но без асинхронного вызова внутри, позволяющего чередование), и я думаю, что все должно быть в порядке. Обратите внимание, что в вашем примере назначение в a выполняется после ожидания, а b вызывается перед окончательным ожиданием.
Расширяя пример кода в @freakish ответ, эта категория условий гонки может быть решена путем реализации асинхронного мьютекса. Ниже приведена демонстрация функции, которую я решил назвать using, вдохновленной синтаксисом Оператор C# using:
const lock = new WeakMap();
async function using(resource, then) {
while (lock.has(resource)) {
try {
await lock.get(resource);
} catch {}
}
const promise = Promise.resolve(then(resource));
lock.set(resource, promise);
try {
return await promise;
} finally {
lock.delete(resource);
}
}
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
let x = 1;
const mutex = {};
async function foo() {
await delay(500);
await using(mutex, async () => {
let y = x;
await delay(500);
x = y + 1;
});
await delay(500);
}
async function main() {
console.info(`initial x = ${x}`);
await Promise.all([foo(), foo(), foo()]);
console.info(`final x = ${x}`);
}
main();const lock = new WeakMap();
async function using(resource, then) {
while (lock.has(resource)) {
try {
await lock.get(resource);
} catch {}
}
const promise = Promise.resolve(then(resource));
lock.set(resource, promise);
try {
return await promise;
} finally {
lock.delete(resource);
}
}
using работает, связывая promise с resource, когда контекст получает ресурс, а затем удаляет это обещание, когда оно разрешается из-за того, что ресурс впоследствии освобождается контекстом. Остальные параллельные контексты будут пытаться получить ресурс каждый раз, когда разрешается связанное обещание. Первому контексту удастся получить ресурс, потому что он обнаружит, что lock.has(resource) — это false. Остальные заметят, что lock.has(resource) является true после того, как первый контекст приобрел его, и будут ждать нового промиса, повторяя цикл.
let x = 1;
const mutex = {};
Здесь пустой объект создается как назначенный mutex, потому что x является примитивом, что делает его неотличимым от любой другой переменной, которая связывает то же значение. Нет смысла «использовать 1», потому что 1 не относится к привязке, это просто значение. Однако имеет смысл «использовать x», поэтому, чтобы выразить это, mutex используется с пониманием того, что это представляет право собственностиx. Вот почему lock является WeakMap — он предотвращает случайное использование примитивного значения в качестве мьютекса.
async function foo() {
await delay(500);
await using(mutex, async () => {
let y = x;
await delay(500);
x = y + 1;
});
await delay(500);
}
В этом примере только временной интервал 0,5 с, который фактически увеличивает x, делается взаимоисключающим, что может быть подтверждено разницей во времени примерно в 2,5 с между двумя напечатанными выводами в приведенной выше демонстрации. Увеличение x гарантированно будет атомарной операцией, потому что этот раздел является взаимоисключающим.
async function main() {
console.info(`initial x = ${x}`);
await Promise.all([foo(), foo(), foo()]);
console.info(`final x = ${x}`);
}
main();
Если бы каждый foo() выполнялся полностью параллельно, разница во времени составила бы 1,5 с, но поскольку 0,5 с из них являются взаимоисключающими среди 3 одновременных вызовов, дополнительные 2 вызова вносят еще одну задержку в 1 с, что в сумме составляет 2,5 с.
Для полноты приведем базовый пример без использования мьютекса, демонстрирующий неудачу неатомарного увеличения x:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
let x = 1;
// const mutex = {};
main();
async function foo() {
await delay(500);
// await using(mutex, async () => {
let y = x;
await delay(500);
x = y + 1;
// });
await delay(500);
}
async function main() {
console.info(`initial x = ${x}`);
await Promise.all([foo(), foo(), foo()]);
console.info(`final x = ${x}`);
}Обратите внимание, что общее время составляет 1,5 с, а окончательное значение x неверно из-за состояния гонки, вызванного удалением мьютекса.
Вероятно, вам следует использовать lock.set(resource, promise.catch(e => void e)), чтобы избежать отклонения обещания (например, using(…, async () => { throw new Error(); }) влияет на другие контексты, ожидающие того же ресурса
@Bergi, это хороший момент, но я хочу быть осторожным, потому что это приведет к микрозадаче, в которой вызов foo() пропустит очередь и немедленно получит ресурс непосредственно перед тем, как все ожидающие вызовы смогут возобновить одну микрозадачу позже.
Ничто не может «пропустить» очередь, потому что она по-прежнему одинакова для каждого звонящего? Конечно, ресурс заблокирован на одну микрозадачу дольше, чем потребовалось then(), но это не имеет значения.
@Bergi Берги Я решил поймать ошибку в спин-блокировке. Я считаю, что, в отличие от .catch(), это не увеличивает количество используемых микрозадач, но также кажется немного чище, чтобы справиться с этим там.
Вы можете использовать promise-queue в теле c(), чтобы независимо от того, сколько раз он вызывался «параллельно», код внутри всегда выполнялся последовательно.
$ npm install promise-queue
const Queue = require('promise-queue');
class TestClass {
constructor() {
this.queue = new Queue(1);
}
// ...
async c(val) {
// The queued function will not execute until all
// earlier functions in the queue have finished.
//
// So execution will always be: a, b, a, b,
// and never: a, a, b, b.
//
await this.queue.add(async () => {
await this.a(val)
// do more stuff
await otherFunction(this.b())
});
}
}
Вы также можете переместить очередь за пределы класса, если вам нужна одна очередь для всех экземпляров класса.
const globalTestQueue = new Queue(1);
A race condition is an undesirable situation that occurs when a device or system attempts to perform two or more operations at the same time, but because of the nature of the device or system, the operations must be done in the proper sequence to be done correctly.
Простым примером состояния гонки может быть один источник света, подключенный к двум разным контроллерам освещения. Когда мы наводим на источник света двумя одновременными действиями — одно включается, а другое выключается. Неопределенный конечный результат, независимо от того, включен источник света или нет.
Асинхронизация Javascript не может быть параллельной, поскольку Javascript выполняется в одном потоке, но она работает с параллелизмом.
Если мы запустим эту программу на компьютере с одним ядром ЦП, ОС будет переключаться между двумя потоками, позволяя выполняться одному потоку за раз.
Если мы запустим эту программу на компьютере с многоядерным процессором, мы сможем запускать два потока параллельно — бок о бок в одно и то же время.
Но состояние гонки может возникнуть с любыми асинхронными операциями, независимо от того, происходит ли это с параллелизмом или параллелизмом.
Пусть будет более четкий сценарий, который может вызвать состояние гонки в javascript. Предположим, у нас есть такой сценарий:
Теперь давайте подумаем о следующем случае:
Теперь, если по какой-то причине для первой выборки потребовалось 5 секунд, а для второй выборки - 1 секунда (сигнал сети стал лучше, или размер выборки/вычисления намного меньше, или мы получили не такой загруженный сервер для второй раз), что теперь будет?
У нас есть этот метод для получения данных и их обновления через updateState.
fetch(url)
.then(data => data.json())
.then(data => updateState(data));
В этом случае произойдет какая-то странная асинхронная ошибка, мы смотрим на область 2, но видим элементы области 1.
Некоторые решения…
Итак, вы спрашиваете меня, что я могу сделать?
Вы можете сохранить последний запрос и отменить его при следующем. На момент написания этой статьи у fetch нет API отмены (https://github.com/whatwg/fetch/issues/27). Но ради аргумента вот код с setTimeout:
if (this.lastRequest) {
clearTimeout(this.lastRequest);
}
this.lastRequest = setTimeout(() => {
updateState([1,2,3]);
this.lastRequest = null;
}, 5000);
Вы можете создавать объект сеанса при каждом новом запросе и сохранять его, а при проверке ответа сохраненный объект остается таким же, как тот, который у вас есть:
const currentSession = {};
this.lastSession = currentSession;
fetch(url)
.then(data => data.json())
.then(items =>{
if (this.lastSession !== currentSession) {
return;
}
updateState(items);
}).catch(error =>{
if (this.lastSession !== currentSession){
return;
}
setError(error);
});
Ресурсы.
Есть ли состояние гонки в Javascript: да и нет
Это действительно слишком упрощено, так как просто нет причин для
aбытьasync. Вы можете сделать этоawaitчто-то