Понимание цикла событий

135

Я думаю об этом, и вот что я придумал:

Посмотрим на этот код ниже:

console.clear();
console.log("a");
setTimeout(function(){console.log("b");},1000);
console.log("c");
setTimeout(function(){console.log("d");},0);

Приходит запрос, и JS-движок начинает шаг за шагом выполнять приведенный выше код. Первые два вызова - это вызовы синхронизации. Но когда дело доходит до setTimeoutметода, он становится асинхронным. Но JS немедленно возвращается из него и продолжает выполнение, которое называется Non-Blockingили Async. И он продолжает работать над другими и т. Д.

Результаты этого исполнения следующие:

AcDb

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

Здесь мы говорим об однопоточном приложении. JS Engine продолжает выполнять это, и если он не завершит первый запрос, он не перейдет ко второму. Но хорошо то, что он не будет ждать, пока блокирующие операции, например, setTimeoutразрешатся, поэтому он будет быстрее, потому что он принимает новые входящие запросы.

Но мои вопросы возникают по следующим пунктам:

№1: Если мы говорим об однопоточном приложении, то какой механизм обрабатывает, в setTimeoutsто время как JS-движок принимает больше запросов и выполняет их? Как отдельный поток продолжает работать над другими запросами? Что работает, в setTimeoutто время как другие запросы продолжают поступать и выполняться.

# 2: Если эти setTimeoutфункции выполняются за кулисами, в то время как поступает и выполняется больше запросов, что за кулисами выполняет асинхронное выполнение? Что это за вещь, о которой мы говорим, называется EventLoop?

# 3: Но не следует ли поместить весь метод в EventLoopтак, чтобы все это выполнялось и вызывается метод обратного вызова? Вот что я понимаю, когда говорю о функциях обратного вызова:

function downloadFile(filePath, callback)
{
   blah.downloadFile(filePath);
   callback();
}

Но в этом случае, как JS Engine узнает, является ли это асинхронной функцией, чтобы он мог поместить обратный вызов в EventLoop? Возможно, что-то вроде asyncключевого слова в C # или какого-то атрибута, который указывает, что метод, который будет использовать JS Engine, является асинхронным и должен обрабатываться соответствующим образом.

# 4: Но в статье говорится совершенно противоположное тому, что я предполагал о том, как все может работать:

Цикл событий - это очередь функций обратного вызова. Когда выполняется асинхронная функция, функция обратного вызова помещается в очередь. Механизм JavaScript не начинает обработку цикла событий, пока не будет выполнен код после выполнения асинхронной функции.

# 5: И вот это изображение, которое может быть полезно, но первое объяснение на изображении говорит то же самое, что упоминалось в вопросе номер 4:

введите описание изображения здесь

Итак, мой вопрос заключается в том, чтобы получить некоторые пояснения по перечисленным выше пунктам?

Тарик
источник
1
Потоки - не подходящая метафора для решения этих проблем. Подумайте о событиях.
Denys Séguret 06
1
@dystroy: Было бы неплохо увидеть образец кода, иллюстрирующий эту метафору события в JS.
Tarik
Я не понимаю, в чем именно ваш вопрос.
Denys Séguret 06
1
@dystroy: У меня вопрос, чтобы получить разъяснения по перечисленным выше пунктам?
Tarik
2
Node не является однопоточным, но для вас это не имеет значения (кроме того, что ему удается выполнять другие задачи, пока выполняется ваш пользовательский код). Одновременно выполняется не более одного обратного вызова в вашем пользовательском коде.
Denys Séguret

Ответы:

85

1: Если мы говорим об однопоточном приложении, то что обрабатывает setTimeouts, в то время как движок JS принимает больше запросов и выполняет их? Разве этот единственный поток не продолжит работу над другими запросами? Тогда кто будет продолжать работать над setTimeout, в то время как другие запросы продолжают поступать и выполняться.

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

Теперь о «выполнении setTimeouts», когда вы вызываете setTimeout, все, что делает узел, это в основном обновляет структуру данных функций, которые будут выполняться одновременно в будущем. По сути, у него есть куча очередей вещей, которые нужно сделать, и каждый «тик» цикла событий он выбирает один, удаляет его из очереди и запускает.

Ключевым моментом является понимание того, что большую часть тяжелой работы узел полагается на ОС. Таким образом, входящие сетевые запросы фактически отслеживаются самой ОС, и когда узел готов обработать один, он просто использует системный вызов, чтобы запросить у ОС сетевой запрос с данными, готовыми к обработке. Так большая часть "работы" узла ввода-вывода - это либо "Эй, ОС, есть ли сетевое соединение с данными, готовыми к чтению?" или «Привет, ОС, у любого из моих невыполненных вызовов файловой системы есть данные?». Основываясь на своем внутреннем алгоритме и конструкции механизма цикла событий, узел выберет один «тик» JavaScript для выполнения, запустит его, а затем повторит процесс заново. Вот что подразумевается под циклом событий. По сути, Node всегда определяет, «какой следующий кусочек JavaScript мне следует запустить?», А затем запускает его.setTimeoutили process.nextTick.

2: Если эти setTimeout будут выполняться за кулисами, в то время как больше запросов поступают, поступают и выполняются, то за кулисами выполняется асинхронное выполнение, о котором мы говорим EventLoop?

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

3: Как JS Engine может узнать, является ли это асинхронной функцией, чтобы поместить ее в EventLoop?

В ядре узла есть фиксированный набор функций, которые являются асинхронными, потому что они выполняют системные вызовы, и узел знает, какие это, потому что они должны вызывать ОС или C ++. В основном все операции ввода-вывода в сети и файловой системе, а также взаимодействия с дочерними процессами будут асинхронными, и ЕДИНСТВЕННЫЙ способ, которым JavaScript может заставить узел запускать что-то асинхронно, - это вызвать одну из асинхронных функций, предоставляемых базовой библиотекой узла. Даже если вы используете пакет npm, который определяет свой собственный API, чтобы создать цикл событий, в конечном итоге этот код пакета npm вызовет одну из асинхронных функций ядра узла, и когда узел узнает, что тик завершен, и он может запустить событие алгоритм цикла снова.

4 Цикл событий - это очередь функций обратного вызова. Когда выполняется асинхронная функция, функция обратного вызова помещается в очередь. Механизм JavaScript не начинает обработку цикла событий, пока не будет выполнен код после выполнения асинхронной функции.

Да, это правда, но это заблуждение. Ключевым моментом является то, что нормальный паттерн:

//Let's say this code is running in tick 1
fs.readFile("/home/barney/colors.txt", function (error, data) {
  //The code inside this callback function will absolutely NOT run in tick 1
  //It will run in some tick >= 2
});
//This code will absolutely also run in tick 1
//HOWEVER, typically there's not much else to do here,
//so at some point soon after queueing up some async IO, this tick
//will have nothing useful to do so it will just end because the IO result
//is necessary before anything useful can be done

Итак, да, вы можете полностью заблокировать цикл событий, просто подсчитывая числа Фибоначчи синхронно в памяти и за один тик, и да, это полностью остановит вашу программу. Это кооперативный параллелизм. Каждый тик JavaScript должен приводить к возникновению цикла событий в течение некоторого разумного периода времени, иначе архитектура в целом выйдет из строя.

Питер Лайонс
источник
1
Допустим, у меня есть очередь, выполнение которой займет у сервера 1 минуту, и первым делом была некоторая асинхронная функция, которая завершилась через 10 секунд. Пойдет ли он в конец очереди или встанет в очередь, как только будет готов?
ilyo
4
Как правило, он идет в конец очереди, но семантика process.nextTickvs setTimeoutvs setImmediateнемного отличается, хотя вам не о чем беспокоиться. У меня есть сообщение в блоге под названием setTimeout и друзья , в котором рассказывается более подробно.
Питер Лайонс
Можете ли вы уточнить? Скажем, у меня есть два обратных вызова, и у первого есть метод changeColor со временем выполнения 10 мс и setTimeout 1 минута, а у второго есть метод changeBackground со временем выполнения 50 мс с setTimeout 10 секунд. Я чувствую, что changeBackground сначала будет в очереди, а следующим будет changeColor. После этого цикл событий синхронно выбирает методы. Я прав?
SheshPai
1
@SheshPai слишком запутанно обсуждать код, написанный абзацами на английском языке. Просто опубликуйте новый вопрос с фрагментом кода, чтобы люди могли отвечать на основе кода, а не описания кода, что оставляет много двусмысленности.
Питер Лайонс
youtube.com/watch?v=QyUFheng6J0&spfreload=5 это еще одно хорошее объяснение движка JavaScript
Мукеш Кумар
65

Филип Робертс подготовил фантастический видеоурок, который объясняет цикл событий javascript самым упрощенным и концептуальным образом. Каждый разработчик javascript должен посмотреть.

Вот ссылка на видео на Youtube.

sam100rav
источник
16
Я смотрел это, и это действительно было лучшее объяснение на свете.
Tarik
1
Обязательно посмотрите видео для поклонников и энтузиастов JavaScript.
Nirus 01
1
это видео меняет мою жизнь ^^
HuyTran
1
бродил сюда ... и это одно из лучших объяснений, которые я получил ... спасибо, что поделились ...: D
RohitS
1
Это было открытием для глаз
HebleV
11

Не думайте, что хост-процесс является однопоточным, это не так. Однопоточным является часть хост-процесса, выполняющая ваш код javascript.

За исключением фоновых рабочих , но они усложняют сценарий ...

Итак, весь ваш js-код выполняется в одном потоке, и нет возможности, что вы получите две разные части вашего js-кода для одновременного запуска (так что вам не придется управлять параллелизмом).

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

Вот мое мысленное представление (осторожно: просто я не знаю деталей реализации браузера!) Вашего примера кода:

console.clear();                                   //exec sync
console.log("a");                                  //exec sync
setTimeout(                //schedule inAWhile to be executed at now +1 s 
    function inAWhile(){
        console.log("b");
    },1000);    
console.log("c");                                  //exec sync
setTimeout(
    function justNow(){          //schedule justNow to be executed just now
        console.log("d");
},0);       

Пока ваш код выполняется, другой поток в хост-процессе отслеживает все происходящие системные события (щелчки пользовательского интерфейса, чтение файлов, полученные сетевые пакеты и т. Д.)

Когда ваш код завершается, он удаляется из цикла событий, и хост-процесс возвращается к его проверке, чтобы увидеть, есть ли еще код для запуска. Цикл событий содержит еще два обработчика событий: один должен быть выполнен сейчас (функция justNow), а другой - в течение секунды (функция inAWhile).

Теперь хост-процесс пытается сопоставить все произошедшие события, чтобы увидеть, зарегистрированы ли для них обработчики. Он обнаружил, что событие, которого ожидает justNow, произошло, поэтому он начал запускать свой код. При выходе из функции justNow она проверяет цикл событий в другой раз, ища обработчики событий. Предположим, что прошла 1 с, запускается функция inAWhile и так далее ....

Андреа Пароди
источник
Однако setTimeout реализован в основном потоке. Таким образом, в вашем примере нет ничего, что требует отдельного потока. Фактически, в браузерах только вкладки реализованы в несколько потоков. На одной вкладке все процессы, включая создание нескольких параллельных сетевых подключений, ожидание щелчков мыши, setTimeout, анимацию и т. Д.,
Выполняются