2016-09-05 1 views
1

Предположим, я запустил этот кусок кода.Data Races в JavaScript?

var score = 0; 
for (var i = 0; i < arbitrary_length; i++) { 
    async_task(i, function() { score++; }); // increment callback function 
} 

В теории я понимаю, что это представляет собой гонки данных и два потока пытаются увеличивать в то же время может привести одно приращение, однако, nodejs (и JavaScript), как известно, однопоточный. Я уверен, что окончательное значение балла будет равно произвольной длине?

ответ

1

Узел использует цикл событий. Вы можете думать об этом как о очереди. Поэтому мы можем предположить, что ваш цикл for помещает function() { score++; } callback arbitrary_length раз в эту очередь. После этого двигатель js запускает их один за другим и каждый раз увеличивает score. Так да. Единственное исключение, если обратный вызов не вызывается или переменная 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); 
    }); 
} 
+0

«вызов одного обратного вызова при выполнении каждой задачи». Как я могу узнать, что все задачи выполнены. Я могу думать только о том, что занят до тех пор, пока не заработает счет! = Произвольный_length – bilalba

+0

@bilalba Да, это все, что вы можете сделать. На самом деле этот код делает то же самое с длиной массива результатов. :-) Лучше кстати. добавить какой-то обработчик ошибок для отклоненных задач, иначе вы будете ждать вечности при возникновении ошибки. – inf3rno

+1

«Как я могу узнать, что все задачи выполнены» <- Это точная причина, по которой «Promise.all()» был реализован. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all –

1

В то же время не может выполняться два вызова функции (узел b/c - однопоточный), так что это не будет проблемой. Единственной проблемой было бы, если в некоторых случаях async_task (..) отбрасывает обратный вызов. Но если, например, «async_task (..)» просто вызывал setTimeout (..) с данной функцией, тогда да, каждый вызов будет выполняться, они никогда не будут сталкиваться друг с другом, а «оценка» будет иметь ожидаемое значение , 'произвольная_ длина', в конце.

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

2

Am I гарантирует, что окончательная стоимость нот будет равна arbitrary_length?

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

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

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

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

Подробнее о событии, управляемом природой Javascript здесь: How does JavaScript handle AJAX responses in the background?, и этот ответ также содержит ряд других ссылок.

И другой подобный ответ, который обсуждает вид общих условий гонки данных, которые возможны: Can this code cause a race condition in socket io?

Некоторые другие ссылки:

how do I prevent event handlers to handle multiple events at once in javascript?

Do I need to be concerned with race conditions with asynchronous Javascript?

JavaScript - When exactly does the call stack become "empty"?

Node.js server with multiple concurrent requests, how does it work?


Чтобы дать вам представление о вопросах параллельности, которые могут произойти в Javascript (даже без резьбы и без прерываний, вот пример из моего собственного кода.

У меня есть сервер Raspberry Pi node.js, который управляет вентиляторами в моем доме. Каждые 10 секунд он проверяет два температурных датчика, один внутри чердака и один вне дома, и решает, как он должен управлять вентиляторами (через реле). Он также записывает данные о температуре, которые могут быть представлены в диаграммах. Один раз в час он сохраняет последние данные температуры, собранные в памяти, в некоторые файлы для сохранения в случае сбоя питания или сбоя сервера. Эта операция сохранения включает в себя ряд асинхронных файлов. Каждая из этих асинхронных записей возвращает управление системе, а затем продолжается, когда асинхронный обратный вызов называется завершением сигнализации. Поскольку это низкая система памяти, и данные могут потенциально занимать значительную часть доступной ОЗУ, данные не копируются в памяти перед записью (это просто непрактично). Итак, я записываю данные в режиме реального времени на диск.

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

На самом деле у меня есть оператор 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 до тех пор, пока не вернет управление системе, а затем (и только тогда), он выберет следующее событие из очереди событий и вызовет обратный вызов, связанный с этим событием.

Ниже представлена ​​последовательность событий под обложками.

  1. Этот JS начинает работать.
  2. console.log("A") - выход.
  3. Событие таймера - это график на 500 мс с этого момента. Подсистема таймера использует собственный код.
  4. console.log("C") - выход.
  5. Код вводится в контур вращения.
  6. В какой-то момент времени на части через спин-контур предварительно установленный таймер готов к срабатыванию. Решать, как это работает, зависит от реализации интерпретатора, но конечным результатом является то, что событие таймера вставляется в очередь событий Javascript.
  7. Цикл спина завершается.
  8. console.log("D") - выход.
  9. Этот кусок Javascript заканчивается и возвращает управление обратно в систему.
  10. Интерпретатор Javascript видит, что текущий фрагмент Javascript сделан, поэтому он проверяет очередь событий, чтобы увидеть, ожидаются ли ожидающие события ожидания. Он находит событие таймера и обратный вызов, связанный с этим событием, и вызывает этот обратный вызов (запуск нового блока выполнения JS). Этот код начинает работать и выводится console.log("B").
  11. Этот setTimeout() callback завершает выполнение, и интерпретатор снова проверяет очередь событий, чтобы увидеть, есть ли какие-либо другие события, которые готовы к запуску.
+0

Добавлен ряд ссылок. – jfriend00

+0

Это очень проницательный ответ, спасибо вам большое! – bilalba

+0

«Все еще есть некоторые условия гонки, о которых нужно беспокоиться, но они имеют больше общего с общим состоянием, доступным для нескольких асинхронных обратных вызовов». <- Я бы не назвал это условием гонки, а скорее упорядочил ваши асинхронные обратные вызовы так, чтобы они выполнялись последовательно (например, с помощью цепочки 'Promise.then()). Это похоже на то, что у вас есть 3 функции, один из которых проверяет наличие файла, один для его открытия и один для доступа к его содержимому. Это не будет считаться условием гонки, если вы запустили последний первый перед вторым. –