Рубрики
Без рубрики

Демистификация асинхронного программирования Часть 1: Node.js Структура

Как программист Node.js, как вы не можете быть знакомы с поведением асинхронного программирования? Прочитайте этот всеобъемлющий пост для важнейших идей в связи с циклом событий Node.js и шаблон события.

Автор оригинала: Simen Li.

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

Этот пост – моя попытка сделать то, что я только что сказал. Мне потребовалось несколько ночей и почти 20 часов, чтобы написать (я знаю правильно …?!). Я действительно хотел написать более короткую часть, но обнаружил, что я не могу включить все всего за несколько коротких предложений. Однако столько, сколько я писал, есть некоторые вещи, которые поскользнулись. Поэтому, пожалуйста, не стесняйтесь, дайте мне знать, если вы найдете что-то не так, чтобы мы могли сделать этот пост как можно точнее!

Короче : Знаете ли вы, что вы больше не можете использовать Process.NextTick () Разделить ваши долгосрочные задачи? Если у вас уже есть хорошее понимание асинхронной природы Node.js, вы можете пропустить впереди на «Предупреждение« »!|« Часть этого поста, чтобы узнать больше о официальном пользовании Node.js дал разработчикам.

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

Во-первых, фрагмент кода: взять на себя догадка на выходном порядке Console.log

Чтобы намочить ноги, давайте посмотрим на код ниже. Какой порядок выходных сообщений? Давайте проверим свои знания у Node.js асинхронного поведения.

console.log('<0> schedule with setTimeout in 1-sec');
setTimeout(function () {
    console.log('[0] setTimeout in 1-sec boom!');
}, 1000);

console.log('<1> schedule with setTimeout in 0-sec');
setTimeout(function () {
    console.log('[1] setTimeout in 0-sec boom!');
}, 0);

console.log('<2> schedule with setImmediate');
setImmediate(function () {
    console.log('[2] setImmediate boom!');
});

console.log('<3> A immediately resolved promise');
aPromiseCall().then(function () {
    console.log('[3] promise resolve boom!');
});

console.log('<4> schedule with process.nextTick');
process.nextTick(function () {
    console.log('[4] process.nextTick boom!');
});

function aPromiseCall () {
    return new Promise(function(resolve, reject) {
        return resolve();
    });
}

Выход:

<0> schedule with setTimeout in 1-sec
<1> schedule with setTimeout in 0-sec
<2> schedule with setImmediate
<3> A immediately resolved promise
<4> schedule with process.nextTick
[4] process.nextTick boom!
[3] promise resolve boom!
[1] setTimeout in 0-sec boom!
[2] setImmediate boom!
[0] setTimeout in 1-sec boom!

( Примечание: На вашем компьютере выходы [1] и [2] может произойти в другом порядке от моего)

Хорошо, давайте посмотрим, что произойдет, если эти вещи произойдут в обратном вызове ввода/вывода. Ниже приведен тот же код, фаршированный в readfile () , функция обратного вызова из асинхронного ввода/вывода API:

var fs = require('fs');

fs.readFile('./file.txt', 'utf8', function (err, data) {
    if (!err) {
        console.log('[I/O Callback get called] ' + data  + '\n');

        console.log('<0> schedule with setTimeout in 1-sec');
        setTimeout(function () {
            console.log('[0] setTimeout in 1-sec boom!');
        }, 1000);

        console.log('<1> schedule with setTimeout in 0-sec');
        setTimeout(function () {
            console.log('[1] setTimeout in 0-sec boom!');
        }, 0);

        console.log('<2> schedule with setImmediate');
        setImmediate(function () {
            console.log('[2] setImmediate boom!');
        });

        console.log('<3> A immediately resolved promise');
        aPromiseCall().then(function () {
            console.log('[3] promise resolve boom!');
        });

        console.log('<4> schedule with process.nextTick');
        process.nextTick(function () {
            console.log('[4] process.nextTick boom!');
        });
    }
});

function aPromiseCall () {  // ... 

Выход:

[I/O Callback get called] read file boom!

<0> schedule with setTimeout in 1-sec
<1> schedule with setTimeout in 0-sec
<2> schedule with setImmediate
<3> A immediately resolved promise
<4> schedule with process.nextTick
[4] process.nextTick boom!
[3] promise resolve boom!
[2] setImmediate boom!
[1] setTimeout in 0-sec boom!
[0] setTimeout 1-sec boom!

( Примечание: На вашем компьютере выходы [2] и [1] определенно произойдут в том же порядке, что и мой)

Теперь успокойся и давайте посмотрим на асинхронные механизмы Node.js. Я имею в виду, действительно спокойно, потому что я искренне надеюсь, что вы можете получить это, просто чтение через этот пост Однажды Отказ

Warmup: JavaScript Loop и асинхронные механизмы

Многие книги и посты в Интернете кратко покрыли петлю событий JavaScript и его асинхронное поведение. С фактическим опытом реализации я уверен, что каждый разработчик JS имеет грубую идею об этой теме.

Если вам не очень комфортно с темой, вы можете провести некоторое время на просмотр двух видео Филипса Робертса ниже. Первый примерно в 15 минутах и сделан очень хорошо. Он кратко описывает, как механизмы JS, такие как одиночная нить, один стек вызова и очереди обратного вызова, работают вместе – это очень легко понять. Второй – это презентация, которую он сделал в JSConfeu – это в значительной степени о том же вещах, что и первый, но имеет дополнительную демонстрацию WebApp. Вы можете пропустить его, если хотите. Конечно, если вы знаете свои вещи, вы можете пойти дальше и пропустить оба видео.

Что происходит в нашем браузере, довольно легко понять. Это немного сложнее, если мы говорим о Node.js, в отличие от браузера, хотя. Node.js использует двигатель Google V8 JavaScript при использовании собственного Libuv обрабатывать ввод/вывод. libuv помещает элементы управления ввода/вывода из разных ОС в пакете, чтобы обеспечить однородное асинхронное/неблокирующее API и установить петли событий. Когда мы говорим о петлях событий Node.js, это будет связано с Libuv.

Node.js.

Node.js действительно одиночная нить?

Когда люди говорят о Node.js, много раз они скажут, что он работает в одной резьбе среды, но на практике она проводит несколько потоков под. В его посте Как отслеживать проблемы процессоров в Node.js Даниэль Хан начинает с приложения Node Application app.js и показывает все процессы, которые работают в нем. Я позволю мистеру Хан рассказать вам об этом (я не могу сказать это лучше себя)

Знаменитое заявление «Node.js работает в одном потоке» только частично верно. На самом деле только код вашей «пользователей» работает в одном потоке. Запуск простого узла приложения и глядя на процессы, показывает, что Node.js на самом деле вращает ряд потоков. Это связано с тем, что Node.js поддерживает пул резьбы для делегирования синхронных задач, пока Google V8 создает свои собственные потоки для задач, таких как сборщик мусора.

Node.js Несколько нить

Libuv Coovers и петлевая итерация

В Libuv Core есть UV_RUN () функция. Первый параметр функции является указателем, который указывает на структуру, uv_loop_t Отказ Здесь uv_loop_t Структура – это контур событий. Каждый раз UV_RUN () Выполняется, считается итерацией петли события ( UV_RUN () – глагол здесь).

UV_RUN () Функция четко написана и легко рассуждать. Нам не нужно портировать подробности; Все, что нам нужно сделать, это знать, что, когда функция выполняется, она работает в функциях порядка, которая включает в себя: UV__UPDATE_TIME () , UV__run_Timers () , UV__run_Pendings () , … и UV__run_clozing_handles () Отказ Каждая функция называется фаза контура событий. Контур событий пройдет все эти фазы, когда он работает от начала до конца.

Ниже приведен часть Libuv Core.CC ( Core.CC Исходный код ), который вы можете просто пройти.

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  // ... 
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      // ... 
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    // ... 
  return r;
}

Объяснение петли событий от официального документации Node.js

Ребята в Node.js были достаточно добрыми, чтобы прикрепить документ с исходным кодом, который очень кратко объясняет Как работают петли событий Таким образом, мы можем узнать вещь или два о петлях событий, не кропотливо идущих над исходным кодом.

Я выделил следующую диаграмму от фигуры в документе и добавил фазовые функции для задач для сравнения бокового побочного времени. Это должно быть достаточно ясно. Я думаю, что каждый разработчик Node.js должен хорошо посмотреть на этот документ. Если вы действительно лень читать, я подчеркнул некоторые из самых важных моментов ниже. Во-первых, я хочу уточнить что-то о обратных вызовах ввода/вывода в правой части рисунка. Обратные вызовы, такие как системные ошибки (E.G. ECONNREFSED, ошибка сокета), в очередь, и соответствует UV__run_pending () фаза. Для нормального запроса ввода/вывода обратные вызовы выполняются на этапе опроса.

uv_run_times ()

Сводка характеристик цикла событий

  • Каждый этап имеет свою собственную очередь FIFO обратных вызовов, связанных с фазой
  • После входа в фазу фаза будет синхронно запустить обратные вызовы в заказе очереди пока все не будут выполнены (или верхний предел достигнут) перед переходом на следующую фазу.

    • Это почему мы говорим, не запускайте интенсивные задачи в обратном вызове, или контур событий будет заблокирован.
  • Когда итерация петли событий завершена, она проверяет любые асинхронные ввода/вывода или таймеры, которые ждут обработки. Если нет, контур событий выходит.
    • Например, если вы напишите App.js только с линией console.log («Hello») , это выйдет, как только эта линия была выполнена. С другой стороны, если вы пишете HTTP Server.Listen (3000, Функция (), {...}) Как только он будет вызывать, он будет продолжать работать, потому что розетка была открыта в нижних слоях и будет продолжать ожидание события ввода/вывода, если вы не закроете его.

Описание каждой фазы

  • Таймер: Запускает обратные вызовы, добавленные в очередь по Setimeout () и setinterval ()
  • Обратные вызовы ввода/вывода: Каждый обратный вызов, связанный с системными ошибками, в очереди здесь
  • Простой, готовят: внутреннее использование
  • Опрос: Извлекает новые события ввода/вывода из системы и запускает соответствующие обратные вызовы ввода/вывода
  • Проверьте: Запускает обратные вызовы, добавленные в очередь по Setimmediate ()
  • Закрыть обратные вызовы: Слушает обратные вызовы из обратных вызовов событий ввода-вывода (E.g. Socket.on («Закрыть», …))

Как добавить задачу (или обратный вызов) на петлю события

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

  • Использование Setimeout () и SetInterval () таймера.

    • Обратные вызовы добавляются в очередь фазы таймера
  • Использование неблокирующих IO API Libuv
    • Такие как розетки или файловые системы API или асинхронные API, такие как fs.readfile () в узле.
  • Использование Setimmediate ()

    • Обратные вызовы добавляются в очередь контрольной фазы
  • Через Process.NextTick ()

    • Это часть контура события узла, но не является частью какой-либо фазы в Libuv. Я объясню это позже.
  • Есть еще одна вещь, которая не была упомянута в документе – использование Обещание (MicroTask)

    • Это часть контура события узла, но не является частью какой-либо фазы в Libuv. Я объясню это позже.

Как долго именно галочка?

Раньше мы видели Process.NextTick () как часть API. Вы когда-нибудь думали о том, как долго галочка? Это Ответ на Stackoverflow Ответы это вполне кратко:

Тикание требует до тех пор, пока требуется для одной итерации контуров событий, где каждый обратный вызов в очередях был выполнен синхронно и в порядке. Таким образом, продолжительность тика не исправлена. Это может быть долго, это может быть коротко, но мы хотим, чтобы это было максимально коротким. Итак, опять же, все разработчики Node.js скажут вам это: не запускайте длительные задачи в обратном вызове! Что происходит, вы заблокируете контур событий. Когда вы протягиваете галочку, цикл события не сможет проверить события ввода/вывода так часто в заданном количестве времени, что означает терять производительность для вашего асинхронного кода.

Порядок исполнения

Пожалуйста, прочитайте раздел на Фазы подробно От Node.js Документ для более о порядке исполнения. Я подведу его с несколькими ключевыми моментами.

Давайте посмотрим на рисунок ниже. Несмотря на то, что цикл выглядит так, будто она начала на таймерах, даже в Libuv, но это не так. Давайте поставим так, цикл событий – это замкнутая петля – когда он сначала выключается, он действительно начинается с фазы таймеров. Но если вы посмотрите на это достаточно долго, с замкнутым циклом вы можете выбрать любую точку и призвать ее запуск. Целью программы обычно имеет отношение к I/O. Например, если вы откроете сокет, вы можете сказать, что цикл события построен вокруг фазы опроса, потому что вы всегда остановитесь на этапе опроса, сделаете то, что нужно сделать, то осмотрите, чтобы увидеть, есть ли что-то еще это должно быть сделано. Вы увидите, что многие обсуждения в Интернете, включая эту официальную документацию, центр вокруг фазы опроса. Обратите внимание на эту линию от официальной документации:

Технически, контроль фазы опроса при выполнении таймеров.

UV_RUN_TIMERS ()

Официальное объяснение является своего рода повсюду, поэтому позвольте мне обобщить его в соответствии с моим собственным пониманием. Если мы начнем с фазы опроса, заказ будет выглядеть так:

  • Фаза опроса: Сначала обрабатывайте события ввода/вывода, сохраняя охватывающие таймеры, которые собираются истекать. Перейдите дальше, чтобы проверить фазу, когда все было обработано.
  • Фаза проверки: Процесс элементы, добавленные setimmediate () ; Если нет, или если они все были обработаны, затем откатитесь назад к таймерам, чтобы увидеть, будет ли что-либо истечь.
  • Затем переместитесь на фазу опроса: сначала обрабатывайте события ввода/вывода, удерживая глаза на таймерам, которые собираются истекать.
  • Основной принцип
    • Если таймер собирается истечь, но событие ввода/вывода происходит первое, то событие ввода/вывода обрабатывается сначала до обработки истечения срока действия. В результате обратные вызовы таймера не гарантируются вовремя.
    • Применяя пример с официального веб-сайта: если таймер установлен на истечение в 100 мс, но прежде чем происходит событие ввода/вывода, то событие ввода/вывода будет обработано первым, и обратный вызов таймера может быть задержан, скажем, , 105 мс перед выполнением.
  • Преосущественный принцип
    • Каждый обратный вызов в очереди через Process.NextTick () должен быть обработан Порядок и синхронно в конце каждой фазы и до начала следующего.
    • Итак, вы никогда не сможете запускать длительную задачу в Process.NextTick Функция обратного вызова.
    • Не выполняйте функции, которые рекурсивно звонят Process.NextTick Потому что тогда эта фаза всегда обнаружит еще один обратный вызов для запуска, а контур события навсегда заблокирован в этой фазе. (Если кто-то из вас, думаю, я ошибился неправильно после прочтения официальной документации самостоятельно, пожалуйста, дайте мне знать!)

Setimeate () и Setimmediate ()

  • Setimeout () принадлежит к этапу таймеров. Он предназначен для выполнения, когда истекло.
  • setimmediate () принадлежит к этапу проверки. Он предназначен для выполнения после каждого фазы опроса.
  • setimmediate () Не зависит от таймера, но Node.js все еще включал эту функцию API в основных модулях таймеров.
  • Из двух методов, если они называются во время цикла ввода/вывода, обратный вызов Setimmediate (CB) будет работать первым (потому что следующий этап – это этап проверки). Если не вызывается во время цикла ввода/вывода, порядок выполнения Setimmediate (CB) и ноль – второй Settimeout (CB, 0) это не детерминирован.
  • Возвращаясь к «Угадать вывод» упражнения в начале этого поста, вопрос о порядке [1] и [2] Отвечено здесь.

Process.nextTick () и setimmediate ()

  • Process.NextTick не принадлежит ни к какой фазе (объяснено позже)
  • Обратные вызовы, поставленные в очереди Process.NextTick () Все будет ссылаться до завершения текущей фазы. Что означает, если вы рекурсивно звоните Process.NextTick () В очередь никогда не будет пустым, и вы никогда не дойдете до следующего этапа, что вызывает голода ввода/вывода (невозможность для опроса).
  • Если вы рекурсивно звоните setimmediate () Следующий добавленный обратный вызов будет выполнен в следующей итерации петли, поэтому цикл события не будет заблокирован.
  • А потом есть невозможное Process.NextTick () Даже великий Мафинтош спросил это в Twitter в июле прошлого года: «Кто-нибудь имеет хороший код пример того, когда использовать Setimmediate вместо NextTick? »

Предупреждение!

Возможно, вы прочитали книгу, которая учит вас о том, как использовать Process.NextTick () разделить длинные рабочие задачи ». Так как Node.js модифицировал поведение Process.NextTick () Не пытайтесь того, что говорит книга! Контур событий будет заблокирован длительной задачей! Разделительные подзадачи все еще очередят в очереди в том же этапе и выполняются синхронно, что означает, что они так же хороши, как без него! Вы будете лучше разделить их с setimmediate () Отказ С этого дня не будь в заблуждение имени. Никогда не думай, что Process.NextTick можете запланировать вашу задачу для следующего галочка. Это просто просит неприятностей! Вот рекомендация официального документа:

Мы рекомендуем разработчики использовать setimmediate () Во всех случаях, потому что оно проще рассуждать (и оно приводит к коду, что совместимо с более широким разнообразием сред, как браузер JS.)

Node.js Coop

Официальная документация узла упоминает, что Process.NextTick не является частью фаз контура событий в Libuv. Вы можете подумать об этом таким образом: контур событий в узле – это обертка на Libuv Libuv. Внутри этой обертки, но за пределами Libuv есть другие вещи для обработки, которые являются Process.NextTick и микрозаска для обещания. Поэтому, когда мы говорим о цикле события узла, мы имеем в виду петлю события в целом на уровне узла, а не просто экземпляром контура событий в Libuv.

В моем другом посте в блоге Глядя на экспорт и модуль. Экспорты через исходный код Node.js Я упомянул, как пробегает сердечник узла.

StartNodeInstance() -> CreateEnvironment() -> LoadEnvironment()

StartNodeInstance ()

В StartNodeInstance () , UV_RUN () называется, и он вызывается внутри, пока цикла. Это где цикл события уровня узла:

    {
      SealHandleScope seal(isolate);
      bool more;
      do {
        v8::platform::PumpMessageLoop(default_platform, isolate);
        more = uv_run(env->event_loop(), UV_RUN_ONCE);

        if (more == false) {
          v8::platform::PumpMessageLoop(default_platform, isolate);
          EmitBeforeExit(env);

          // Emit `beforeExit` if the loop became alive either after emitting
          // event, or after running some callbacks.
          more = uv_loop_alive(env->event_loop());
          if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
            more = true;
        }
      } while (more == true);
    }

CreateenVironment ()

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

Environment* CreateEnvironment(Isolate* isolate,
                               uv_loop_t* loop,
                               // ... 
                               const char* const* exec_argv) {
  // ... 

  uv_check_init(env->event_loop(), env->immediate_check_handle());
  uv_unref(reinterpret_cast(env->immediate_check_handle()));

  uv_idle_init(env->event_loop(), env->immediate_idle_handle());
  uv_prepare_init(env->event_loop(), env->idle_prepare_handle());
  uv_check_init(env->event_loop(), env->idle_check_handle());
  uv_unref(reinterpret_cast(env->idle_prepare_handle()));
  uv_unref(reinterpret_cast(env->idle_check_handle()));
  // ... 

  return env;
}

startup.processnextTick () (/src/node.js)

Здесь нам нужно только обратить внимание на NextTickceue в начале и Process.NextTick () Отказ Этот метод просто добавляет зарегистрированные обратные вызовы в очередь. Когда _tickcallback () называется, каждый обратный вызов в очереди выполнен. Когда все это сделано, следующий шаг – звонить _Runmicrotasks () продолжать обрабатывать обещания. Если вы хотите узнать больше о MicroTasks, вы можете взглянуть на Задачи, микрозаски, очереди и расписание Написано инженер Google, Jake Archibald. Он также включал небольшой тест о порядке исполнения куска кода … LOL.

Суммировать это, что я пытаюсь сказать, это то, что Process.NextTick () и микрозапромыты принимают наивысший приоритет в асинхронном коде Отказ Они выполняются до конца каждого этапа. (Опять не каждый тик!)

  startup.processNextTick = function() {
    var nextTickQueue = [];   // Callbacks are added to this queue!!
    var pendingUnhandledRejections = [];
    var microtasksScheduled = false;
    var _runMicrotasks = {};
    // ... 
    process.nextTick = nextTick;  // nextTick function below
    // ... 
    // process._setupNextTick is in node.cc. I think once you get the point across, there's no need to go deeper. 
    const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks);
    _runMicrotasks = _runMicrotasks.runMicrotasks;
    // ... 
    function _tickCallback() {
      var callback, args, tock;

      do {
        while (tickInfo[kIndex] < tickInfo[kLength]) {
        // callbacks are dug out of the queue to be executed one by one
          tock = nextTickQueue[tickInfo[kIndex]++];
          callback = tock.callback;
          args = tock.args;

          if (args === undefined) {
            nextTickCallbackWith0Args(callback);
          } else {
            switch (args.length) {
              case 1:
                nextTickCallbackWith1Arg(callback, args[0]);
              // ...
            }
          }
          if (1e4 < tickInfo[kIndex])
            tickDone();
        }
        tickDone();
        // callback functions of process.nextTick finishes, then, promise microtasks
        _runMicrotasks();
        emitPendingUnhandledRejections();
      } while (tickInfo[kLength] !== 0);
    }

    // ...
    function nextTick(callback) {
      var args;
      if (arguments.length > 1) {
        args = [];
        for (var i = 1; i < arguments.length; i++)
          args.push(arguments[i]);
      }

      // store the callbacks along with its aruments in an object and push it into the queue
      nextTickQueue.push(new TickObject(callback, args));
      tickInfo[kLength]++;
    }

    // ...
  };

На данный момент я думаю, что у вас есть суть. Так как настолько много исходного кода, я оставлю его любопытно, чтобы копать в этом сами.

Если вы посмотрите на « NextTick » NextTickceue , вы можете ввести в заблуждение, чтобы подумать, что он выполнен в следующей итерации цикла. Фактически, функции обратного вызова в этой очереди будут выполняться до того, как контур событий подготавливается для фазового перехода. О двусмысленности именования NextTick и Setimmediate Официальная документация также упоминает это:

По сути, имена должны быть поменяются. Process.nextTick ( ) пожаровал больше немедленно, чем setimmediate () Но это артефакт прошлого, который вряд ли изменится. Сделать этот переключатель сломал бы большой процент пакетов на NPM.

Если вы добрались до этого момента, не засыпая, безумие к вам! Теперь не злитесь, но мы наконец добираемся до нашей другой темы: Emitter Embile!

Поверьте мне, этот закончится, прежде чем узнать это. Прочитайте следующую статью здесь

Этот пост был переведен на английский язык по команде контента кодаментатора. Здесь «Это оригинальный китайский пост от Симона Ли.