Гонки данных в JavaScript?
Предположим, я запустил этот кусок кода.
var score = 0; for (var i = 0; i < arbitrary_length; i++) { async_task(i, function() { score++; }); // increment callback function }
В теории я понимаю, что это представляет собой гонку данных, и два потока, пытающихся увеличиваться в одно и то же время, могут привести к одному приращению, однако nodejs (и javascript) известны как однопоточные. Я уверен, что конечное значение балла будет равно произвольной длине?
- Получить базовый URL-адрес моего веб-приложения в JavaScript
- Лучший способ выполнить полнотекстовый поиск в MongoDB и Mongoose
- Создание лямбда-функции в AWS из zip-файла
- Как использовать прототипы, такие как модули в Node.js?
- Вызов API графиков Microsoft изнутри Функции Azure
Узел использует цикл событий. Вы можете думать об этом как о очереди. Поэтому мы можем предположить, что ваш цикл for ставит function() { score++; }
function() { score++; }
Callback arbitrary_length
раз в этой очереди. После этого движок js запускает их один за другим и каждый раз увеличивает score
. Так да. Единственное исключение, если обратный вызов не вызывается или доступ к переменной счетчика происходит из другого места.
Фактически вы можете использовать этот шаблон для выполнения задач параллельно, собирать результаты и вызывать один обратный вызов, когда каждая задача выполняется.
var results = []; for (var i = 0; i < arbitrary_length; i++) { async_task(i, function(result) { results.push(result); if (results.length == arbitrary_length) tasksDone(results); }); }
Я уверен, что конечное значение балла будет равно произвольной длине?
Да, если все async_task()
вызывают обратный вызов один раз и только один раз, вам гарантируется, что окончательное значение балла будет равно произвольной длине.
Это однопоточный характер Javascript, который гарантирует, что в то же самое время никогда не будет работать два Javascript. Вместо этого из-за управляемой событиями Javascript в обоих браузерах и node.js одна часть JS запускается до завершения, затем следующее событие вытягивается из очереди событий и запускает обратный вызов, который также будет запущен до завершения.
Существует не такая вещь, как Javascript с прерываниями (где некоторая обратная связь может прервать часть другого Javascript, который в настоящее время работает). Все сериализуется через очередь событий. Это огромное упрощение и предотвращает множество ситуаций, которые в противном случае были бы очень полезными для безопасной работы при одновременном одновременном использовании нескольких потоков или прерывания.
Все еще есть некоторые проблемы с параллелизмом, которые могут быть затронуты, но они имеют больше общего с общим состоянием, доступ к которому могут иметь множественные асинхронные обратные вызовы. Хотя только один из них когда-либо будет обращаться к нему в любой момент времени, все же возможно, что фрагмент кода, который содержит несколько асинхронных операций, может оставить какое-то состояние в состоянии «между», пока он находился в середине нескольких асинхронных операций на Где может выполняться другая операция async, и может попытаться получить доступ к этим данным.
Вы можете больше узнать о природе Javascript, связанной с событиями: как JavaScript обрабатывает ответы AJAX в фоновом режиме? И этот ответ также содержит ряд других ссылок.
И еще один подобный ответ, в котором обсуждаются условия совместного использования данных данных, которые возможны: может ли этот код вызывать состояние гонки в сокете io?
Некоторые другие ссылки:
Как я могу предотвратить обработчики событий для обработки нескольких событий сразу в javascript?
Должен ли я быть заинтересованным в условиях гонки с асинхронным Javascript?
JavaScript. Когда именно стек вызовов становится «пустым»?
Сервер Node.js с несколькими параллельными запросами, как это работает?
Чтобы дать вам представление о проблемах параллелизма, которые могут возникнуть в Javascript (даже без потоков и без прерываний, вот пример из моего собственного кода.
У меня есть сервер Raspberry Pi node.js, который контролирует поклонников чердака в моем доме. Каждые 10 секунд он проверяет два температурных датчика, один внутри чердака и один вне дома, и решает, как он должен управлять вентиляторами (через реле). Он также записывает данные о температуре, которые могут быть представлены в диаграммах. Один раз в час он сохраняет последние данные о температуре, которые были собраны в памяти, в некоторые файлы для сохранения в случае сбоя питания или сбоя сервера. Эта операция сохранения включает в себя ряд асинхронных файлов. Каждая из этих асинхронных записей возвращает управление системе, а затем продолжается, когда асинхронный обратный вызов называется завершением сигнализации. Поскольку это низкая система памяти, и данные могут потенциально занимать значительную часть доступной ОЗУ, данные не копируются в памяти перед записью (это просто непрактично). Итак, я пишу данные на диске в режиме реального времени.
В любое время во время любой из этих операций ввода-вывода aync-файлов, ожидая, что обратный вызов будет означать завершение многих операций с файлами, один из моих таймеров на сервере может запустить, я собирал новый набор данных температуры и Который попытается изменить набор данных в памяти, который я нахожу в середине написания. Это проблема параллелизма, ожидающая своего появления. Если он изменит данные, пока я написал часть его, и я жду, когда эта запись закончится, прежде чем писать все остальное, тогда полученные данные могут быть легко повреждены, потому что я выписал одну часть данных, Данные будут изменены из-под меня, а затем я попытаюсь выписать больше данных, не понимая, что они были изменены. Это проблема параллелизма.
На самом деле у меня есть оператор console.log()
который явно регистрируется, когда эта проблема параллелизма возникает на моем сервере (и безопасно обрабатывается моим кодом). Это происходит раз в несколько дней на моем сервере. Я знаю, что он есть, и это реально.
Существует много способов обойти эти проблемы параллелизма. Простейшим было бы просто сделать копию в памяти всех данных, а затем выписать копию. Поскольку нет потоков или прерываний, создание копии в памяти было бы безопасно от параллелизма (не было бы уступки асинхронным операциям в середине копии для создания проблемы параллелизма). Но в этом случае это было непрактично. Итак, я выполнил очередь. Когда я начинаю писать, я устанавливаю флаг объекта, который управляет данными. Затем, в любое время, когда система хочет добавлять или изменять данные в сохраненных данных, пока этот флаг установлен, эти изменения просто переходят в очередь. Фактические данные не затрагиваются, пока этот флаг установлен. Когда данные были безопасно записаны на диск, флаг сбрасывается и обрабатываются очереди. Любую проблему параллелизма можно было избежать.
Итак, это пример проблем параллелизма, которые вам нужно беспокоиться. Одно большое упрощающее предположение с Javascript заключается в том, что часть Javascript будет завершена без какого-либо потока прерывания до тех пор, пока он не намеренно возвращает управление системе. Это делает проблемы параллелизма в обработке, как описано выше, намного проще, потому что ваш код никогда не будет прерван, кроме случаев, когда вы сознательно возвращаете систему обратно. Вот почему нам не нужны мьютексы и семафоры и другие подобные вещи в нашем собственном Javascript. Мы можем использовать простые флаги (просто регулярные Javascript-переменные), как я описал выше, если это необходимо.
В любой полностью синхронной части Javascript вы никогда не будете прерваны другим Javascript. Синхронный фрагмент Javascript будет завершен до того, как будет обработано следующее событие в очереди событий. Это означает, что Javascript является «управляемым событиями» языком. В качестве примера этого, если у вас есть этот код:
console.log("A"); // schedule timer for 500 ms from now setTimeout(function() { console.log("B"); }, 500); console.log("C"); // spin for 1000ms var start = Date.now(); while(Data.now() - start < 1000) {} console.log("D");
В консоли вы получите следующее:
A C D B
Событие таймера не может быть обработано до тех пор, пока текущий фрагмент Javascript не будет завершен, даже если он скорее всего будет добавлен в очередь событий раньше этого. То, как работает интерпретатор JS, заключается в том, что он запускает текущую JS до тех пор, пока он не вернет управление системе, а затем (и только тогда), он выберет следующее событие из очереди событий и вызовет обратный вызов, связанный с этим событием.
Вот последовательность событий под обложками.
- Этот JS запускается.
-
console.log("A")
. - Событие таймера – это расписание на 500 мс. Подсистема таймера использует собственный код.
-
console.log("C")
. - Код входит в спиновый цикл.
- В какой-то момент времени часть пути через спин-контур предварительно установленный таймер готов к стрельбе. Решать, как это работает, зависит от реализации интерпретатора, но конечным результатом является то, что событие таймера вставляется в очередь событий Javascript.
- Цикл спина завершается.
-
console.log("D")
. - Этот кусок Javascript заканчивается и возвращает управление системе.
- Интерпретатор Javascript видит, что текущий фрагмент Javascript выполнен таким образом, что он проверяет очередь событий, чтобы увидеть, ожидаются ли ожидающие события ожидания. Он находит событие таймера и обратный вызов, связанный с этим событием, и вызывает этот обратный вызов (запуск нового блока выполнения JS). Этот код запускается, и выводится
console.log("B")
. - Этот callbackTimeout
setTimeout()
завершает выполнение, и интерпретатор снова проверяет очередь событий, чтобы увидеть, есть ли какие-либо другие события, которые готовы к запуску.
В то же время не может выполняться два вызова функции (узел b / c – однопоточный), поэтому это не будет проблемой. Единственная проблема – если в некоторых случаях async_task (..) отменяет обратный вызов. Но если, например, «async_task (..)» просто вызывал setTimeout (..) с данной функцией, то да, каждый вызов будет выполняться, они никогда не будут сталкиваться друг с другом, а «оценка» будет иметь ожидаемое значение , 'Произвольная_ длина', в конце.
Конечно, «произвольная длина» не может быть настолько велика, чтобы выходить из памяти, или переполнять любую коллекцию, удерживающую эти обратные вызовы. Однако проблема с потоками отсутствует.