Должен ли я быть заинтересованным в условиях гонки с асинхронным Javascript?

Предположим, что загружаю какой-то Flash-фильм, который, как я знаю, в какой-то момент в будущем вызовет window.flashReady и установит window.flashReadyTriggered = true.

Теперь у меня есть блок кода, который я хочу выполнить, когда Flash готов. Я хочу, чтобы он выполнял его немедленно, если window.flashReady уже был вызван, и я хочу поместить его в качестве обратного вызова в window.flashReady, если он еще не был вызван. Наивный подход заключается в следующем:

if(window.flashReadyTriggered) {
  block();
} else {
  window.flashReady = block;
}

Итак, проблема, на которой я основываюсь, заключается в том, что выражение в if условие оценивается как false, но затем до block() может быть выполнено, window.flashReady запускается внешней Flash. Следовательно, block никогда не вызывается.

Есть ли лучший шаблон проектирования для достижения цели более высокого уровня, к которой я иду (например, ручного вызова обратного вызова flashReady)? Если нет, я в безопасности, или есть другие вещи, которые я должен делать?

Ответы

Ответ 1

JavaScript однопоточный. Нет условий гонки.

Если для вашего текущего "указателя инструкций" больше нет кода, "поток" "передает эстафету", а обработанный в очереди window.setTimeout или обработчик событий может выполнять свой код.

Вы получите лучшее понимание для однопоточного подхода Javascript, в котором node.js идеи дизайна.

Дальнейшее чтение: Почему JavaScript не поддерживает многопоточность?

Ответ 2

Все сценарии обработчика событий Javascript обрабатываются из одной главной очереди событий. Это означает, что обработчики событий запускаются по одному за раз, и один выполняется до завершения до того, как начнется запуск следующего запуска. Таким образом, в Javascript нет ни одного из типичных условий гонки, которые можно было бы увидеть на многопоточном языке, где одновременно могут выполняться несколько потоков языка (или время нарезания) и конфликт для доступа к переменным. Любой отдельный поток выполнения в javascript запустится до следующего запуска. Это работает Javascript. Событие, выведенное из очереди событий, начинает работать код для обработки этого события. Этот код выполняется сам по себе, пока не вернет управление системе, в которой система вытащит следующее событие из очереди событий и запустит этот код, пока он не вернет управление обратно в систему.

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

Это включает в себя все формы событий Javascript, включая: пользовательские события (мышь, клавиши и т.д.), события таймера, сетевые события (обратные вызовы ajax) и т.д.

Единственное место, которое вы можете сделать многопоточность в Javascript, - это HTML5 Web Workers, но они очень изолированы от обычного javascript (они могут общаться только с обычным javascript через передачу сообщений) и вообще не могут манипулировать DOM и должны иметь свои собственные скрипты и пространство имен и т.д.


Хотя я бы технически не называл это условием гонки, в Javascript есть ситуации из-за некоторых его асинхронных операций, в которых вы можете выполнять две или более асинхронные операции, и это может быть непредсказуемо, когда каждая операция будет завершена относительно других. Это создает неопределенность в отношении времени, которое (если относительное время операций важно для вашего кода) создает что-то, что вы должны вручную запрограммировать. Вам может потребоваться выполнить последовательность операций, чтобы они запускались, и вы буквально дожидаетесь ее завершения до начала следующего. Или вы можете запустить все три операции, а затем получить код, который собирает все три результата, и когда все они будут готовы, тогда ваш код продолжит.

В современном Javascript promises обычно используются для управления этими типами асинхронных операций.

Итак, если у вас три асинхронные операции, каждая из которых возвращает обещание (например, чтение из базы данных, выборка запроса с другого сервера и т.д.), вы можете вручную выполнить следующее:

a().then(b).then(c).then(result => {
    // result here
}).catch(err => {
    // error here
});

Или, если вы хотите, чтобы все они работали вместе (все в полете в одно и то же время) и просто знали, когда все будет сделано, вы можете сделать:

Promise.all([a(), b(), c()])..then(results => {
    // results here
}).catch(err => {
    // error here
});

Хотя я бы не назвал эти условия гонки, они находятся в одном и том же общем семействе проектирования вашего кода для управления неопределенным секвенированием.


В некоторых ситуациях в браузере может возникнуть один особый случай. Это не действительно условие гонки, но если вы используете множество глобальных переменных с временным состоянием, это может быть что-то, о чем нужно знать. Когда ваш собственный код вызывает другое событие, браузер иногда вызывает этот обработчик событий синхронно, а не ждет, пока не будет выполнен текущий поток выполнения. Примером этого является:

  • нажмите
  • обработчик события click переключает фокус на другое поле
  • что в другом поле есть обработчик событий для фокуса
  • браузер немедленно вызывает обработчик события onfocus.
  • Выполняется обработчик событий onfocus
  • выполняется остальная часть обработчика события клика (после вызова .focus())

Это не технически условие гонки, потому что оно известно на 100% при выполнении обработчика события onfocus (во время вызова .focus()). Но он может создать ситуацию, когда один обработчик события работает, а другой находится в середине выполнения.

Ответ 3

Важно отметить, что вы можете столкнуться с условиями гонки, если вы, например. используйте несколько async XMLHttpRequest. Если порядок возвращаемых ответов не определен (то есть ответы могут не возвращаться в том же порядке, в котором они были отправлены). Здесь вывод зависит от последовательности или времени других неконтролируемых событий (задержка сервера и т.д.). Это состояние гонки в двух словах.

Таким образом, даже использование одной очереди событий (например, в JavaScript) не предотвращает появление событий в неконтролируемом порядке, и ваш код должен позаботиться об этом.

Ответ 4

Конечно, вам нужно. Это происходит постоянно:

<button onClick=function() {
  const el = document.getElementById("view");
  fetch('/some/api').then((data) => {
    el.innerHTML = JSON.stringify(data);
  })
}>Button 1</button>

<button onClick=function() {
  const el = document.getElementById("view");
  fetch('/some/other/api').then((data) => {
    el.innerHTML = JSON.stringify(data);
  })

}>Button 2</button>

Некоторые люди не рассматривают его как состояние гонки.

Но это действительно так.

Условие гонки широко определяется как "поведение электронной, программной или другой системы, где выход зависит от последовательности или времени других неконтролируемых событий".

Если пользователь нажимает эти две кнопки в течение короткого периода времени, результат не зависит от порядка нажатия. Это зависит от того, какой запрос api будет разрешен раньше. Более того, элемент DOM, на который вы ссылаетесь, может быть удален другим событием (например, сменой маршрута).

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

Чтобы адаптировать его к вашему вопросу, это зависит от того, что такое "block()". Если это синхронная функция, вам не нужно беспокоиться. Но если это асинхронно, вам нужно беспокоиться:

  function block() {
    window.blockInProgress = true;
    // some asynchronous code
    return new Promise(/* window.blockInProgress = false */);
  }

  if(!window.blockInProgress) {
    block();
  } else {
    window.flashReady = block;
  }

Этот код имеет смысл, вы хотите, чтобы блок не вызывался несколько раз. Но если вам все равно, или "блок" синхронно, вы не должны волноваться. Если вы беспокоитесь о том, что глобальное значение переменной может измениться, когда вы его проверяете, вы не должны беспокоиться, он гарантированно не изменится, если вы не назовете какую-то асинхронную функцию.

Более практичный пример. Рассмотрим, что мы хотим кэшировать запросы AJAX.

 fetchCached(params) {
   if(!dataInCache()) {
     return fetch(params).then(data => putToCache(data));
   } else {
     return getFromCache();
   }
 }

Так происходит, если мы будем называть этот код несколько раз? Мы не знаем, какие данные будут возвращены первыми, поэтому мы не знаем, какие данные будут кэшироваться. Первые 2 раза он вернет свежие данные, но в 3-й раз мы не знаем форму ответа, которую нужно вернуть.