Создает ли этот пример JavaScript «условия гонки»? (В той мере, в какой они могут существовать в JavaScript)

Я знаю, что 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()?

Для справки, я предварительно исправил свою проблему с мьютекс, но я задавался вопросом, была ли проблема с самого начала.

Это действительно слишком упрощено, так как просто нет причин для a быть async. Вы можете сделать это await что-то

Bergi 02.08.2022 09:20

Кроме того, очень легко написать собственный «мьютекс» для принудительного взаимного исключения асинхронных контекстов. Если вам интересно, я могу предоставить ответ, содержащий пример реализации и демонстрацию.

Patrick Roberts 02.08.2022 09:42

Условия гонки - это параллелизм, а не параллелизм. Как вы заметили, JavaScript делает имеет параллелизм в форме async/await, где вы можете чередовать несколько «логических» потоков. В JavaScript отсутствует параллелизм (т. е. наличие нескольких потоков выполнения, работающих в один и тот же момент). stackoverflow.com/questions/1050222/…

GACy20 02.08.2022 16:53

@Bergi Вы правы ... хотя я думаю, что это было достаточно ясно для меня, я изменил пример, сделав его «настоящей» асинхронной функцией, чтобы сделать его более понятным для будущих читателей.

hryrbn 02.08.2022 19:38

Ах, это хотя бы один - да, состояние гонки все еще есть, но это очень редко, так как вам нужно точно определить время микрозадачи. В частности, this.value может быть заменен какой-либо другой микрозадачей во время await перед this.a(val), в противном случае .value выглядит так, как будто this.value = await val; this.b() используется «сразу» после его назначения. (Обратите внимание, что не было бы проблемы, если бы вы написали a в том же методе). Состояние гонки было бы более очевидным, если бы this.value = returnsValue(); await delay(1000) сделал this.value

Bergi 03.08.2022 11:15
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
25
5
2 245
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Да, условия гонки могут возникать и возникают и в 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 03.08.2022 03:15

@Bergi, это хороший момент, но я хочу быть осторожным, потому что это приведет к микрозадаче, в которой вызов foo() пропустит очередь и немедленно получит ресурс непосредственно перед тем, как все ожидающие вызовы смогут возобновить одну микрозадачу позже.

Patrick Roberts 03.08.2022 05:30

Ничто не может «пропустить» очередь, потому что она по-прежнему одинакова для каждого звонящего? Конечно, ресурс заблокирован на одну микрозадачу дольше, чем потребовалось then(), но это не имеет значения.

Bergi 03.08.2022 09:42

@Bergi Берги Я решил поймать ошибку в спин-блокировке. Я считаю, что, в отличие от .catch(), это не увеличивает количество используемых микрозадач, но также кажется немного чище, чтобы справиться с этим там.

Patrick Roberts 04.08.2022 07:27

Вы можете использовать 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 выполняется в одном потоке, но она работает с параллелизмом.

В чем разница между параллелизмом и параллелизмом?

difference between concurrency and paralleslism

Если мы запустим эту программу на компьютере с одним ядром ЦП, ОС будет переключаться между двумя потоками, позволяя выполняться одному потоку за раз.

Если мы запустим эту программу на компьютере с многоядерным процессором, мы сможем запускать два потока параллельно — бок о бок в одно и то же время.

difference between concurrency and paralleslism

Но состояние гонки может возникнуть с любыми асинхронными операциями, независимо от того, происходит ли это с параллелизмом или параллелизмом.

Пусть будет более четкий сценарий, который может вызвать состояние гонки в javascript. Предположим, у нас есть такой сценарий:

  • У нас есть карта
  • Мы хотим показать некоторую информацию, связанную с местоположением пользователя.
  • Каждый раз, когда пользователь перемещается по карте, мы получаем данные.
  • Мы умны и даже отклоняем выборки на 0,5 секунды.
  • Данные возвращаются через 5 секунд (у нас медленная сеть).

Теперь давайте подумаем о следующем случае:

  • Пользователь открывает карту
  • Первый запрос на выборку запущен
  • Через 2 секунды пользователь снова перемещается по карте, потому что ему кажется, что он нашел что-то интересное.
  • Еще через 0,5 секунды запускается еще одна выборка.

Теперь, если по какой-то причине для первой выборки потребовалось 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: да и нет

В чем разница между параллелизмом и параллелизмом?

Как избежать условий асинхронной гонки в JavaScript

Другие вопросы по теме