Обзор

Я считаю, что если вы разработчик Node.js, и не имеет значения младший, средний или старший уровень, вы уже много знаете о ядре Node.js, цикле событий или о том, что Node.js является однопоточным, или о том, как будет обработана функция «setTimeout» или «setimmediate» и так далее.

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

Итак, что такое цикл событий? Является ли Node.js однопоточным или многопоточным?

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

Итак, идея этой статьи состоит в том, чтобы прояснить ваше представление о ядре Node.js, о том, как оно реализовано и как работает. Поскольку Node.js - это больше, чем просто «JavaScript на сервере». Более того, около 30% из них - это C ++, а не JS! И мы собираемся узнать здесь, что эта часть C ++ на самом деле делает в Node.js.

Является ли Node.js однопоточным?

  • Да! И ты прав.
  • Нет! И Вы снова правы.

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

Давайте начнем с самого начала, чтобы узнать, что происходит в Node.js Core?

  • Процессор может выполнять одну задачу за раз или несколько задач (программ) за раз, выполняя их параллельно (Многозадачность).

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

  • Процесс - это контейнер выполнения верхнего уровня. У него есть собственная выделенная система памяти.

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

Https://en.wikipedia.org/wiki/Inter-process_communication

Работа в Unix основана на сокетах. Socket - это число (целое число), которое возвращает системный вызов Socket (); он называется дескриптором сокета или дескриптором файла.

Сокеты указывают на объекты в ядре с виртуальным «интерфейсом» (чтение / запись / пул / закрытие / и т. Д.).

Системные сокеты работают как TCP-сокеты: они конвертируют данные в буфер и только потом отправляют. Поскольку мы используем JavaScript для взаимодействия двух процессов, мы должны вызывать JSON.stringify много раз, но мы знаем, насколько он медленный.

Но подождите, у нас есть темы!

Давайте посмотрим, что это такое и как заставить два потока взаимодействовать.

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

Потоки выполняются в процессах; в одном процессе может быть много потоков, и, поскольку они находятся в одном процессе, они совместно используют память.

Прохладный!!!

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

Но давайте представим, что у нас есть функция в одном потоке, которая записывает в переменную с именем «foo», а другой поток читает из нее. Вопрос в том, что могло случиться?

На самом деле, МЫ НЕ ЗНАЕМ.

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

Вот почему немного сложно писать код в многопоточном режиме. Посмотрим, что для этого говорит Node.js.

Node.js говорит: у меня одна ветка

На самом деле, в Node.js есть V8, и код выполняется в основном потоке, где выполняется цикл обработки событий (поэтому мы говорим, что он однопоточный).

Но, как мы знаем, Node.js - это не просто V8. Существует множество API (C ++), и все это управляется Event Loop, реализованным через libuv (C ++).

C ++ работает за кодом JavaScript и имеет доступ к потокам. Если вы запустите синхронный метод JavaScript, который был вызван из Node.js, он всегда будет выполняться в основном потоке. Но если вы запустите какую-то асинхронную вещь, она не всегда будет выполняться в основном потоке: в зависимости от того, какой метод вы используете, цикл событий может направить его в один из API, и он может быть обработан в другой поток.

Давайте посмотрим на пример CRYPTO. Он имеет много методов, интенсивно использующих процессор; некоторые из них синхронные, некоторые асинхронные. Возьмем метод pbkdf2 (). Если мы запустим его синхронную версию на 2-ядерном процессоре и сделаем 4 вызова, и если время выполнения одного вызова составляет 2 мс, время выполнения всех 4 вызовов будет 4 * время выполнения pbkdf2 () (8 мс).

Но если мы запустим асинхронную версию этого метода на том же ЦП, время выполнения будет 2 * время выполнения pbkdf2 (), потому что процессор будет использовать по умолчанию 4 потока (вы поймете, почему и как ниже), разместите его в двух процессах и обработайте в них pbkdf2 ().

Итак, Node.js работает параллельно за вас, если вы дадите ему шанс. «Так что используйте асинхронные методы» !!!

Node.js использует заранее выделенный набор потоков, называемый пулом потоков, и если мы не укажем, сколько потоков открыть, по умолчанию он откроет 4 потока. Мы можем увеличить его, установив

UV_THREADPOOL_SIZE = 110 && узел index.js

or

process.env.UV_THREADPOOL_SIZE = 62 из кода.

Так является ли Node.js многопоточным?

  • Привет !!! Node.js работает с несколькими потоками! Да! Его многопоточность!

Поэтому, когда люди спрашивают вас, является ли Node многопоточным или однопоточным, вы должны задать дополнительный вопрос: «Когда?».

Давайте посмотрим на TCP-соединения.

Количество потоков на соединение

Самый простой способ создать TCP-сервер - создать сокет, привязать этот сокет к порту и вызвать на нем «прослушивание».

int server = socket();
bind(server, 8080);
listen(server);

Пока мы не вызовем на нем «прослушивание», этот сокет можно использовать для создания соединений или для приема соединений. Когда мы вызываем «слушать», мы готовы принимать соединения.

while(int conn = accept(server)) {
  pthread_create(echo, conn)
}
void echo(int conn) {
  char buf(4096);
  while(int size = read(conn, buffer, sizeof buf)) {
    write(conn, buffer,size);
  }
}

Когда соединение установлено и нам нужно записать в него, мы не можем принять другое соединение, пока не закончим писать, поэтому мы помещаем его в другой поток. Итак, мы передаем потоку дескриптор сокета и указатель на функцию.

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

Единственное, что нам действительно нужно, это дескриптор сокета, и мы должны помнить, что с ним делать. Итак, есть лучший способ: мы можем использовать Epoll (unix), Kqueue (BSD).

Петля Epoll

Давайте сосредоточимся на Epoll, на том, что он может дать нам, в чем причина его использования. Использование Epoll позволяет нам сообщать ядру, какие события нас интересуют, а ядро ​​сообщает, когда происходят вещи, о которых мы спрашивали. В нашем случае это входящее TCP-соединение. Итак, мы создаем дескриптор Epoll и добавляем его в цикл Epoll, вызывая для него «wait». Он просыпается при входящем TCP-соединении, затем мы добавляем его в цикл Epoll и ждем от него данных и так далее. Вот что делает за нас цикл обработки событий!

Возьмем пример:

Когда мы загружаем что-то через запрос (HTTP) на тот же двухъядерный процессор, 4, 6 или даже 8 запросов, это займет то же время. Что это обозначает? Это означает, что ограничения не такие, как у пула потоков.

Это потому, что ОС заботится о загрузке; мы просто просим его скачать, а затем спрашиваем его: Готово? Нет? Законченный? (прослушивание события «данные» в Epoll).

API s

Итак, какой API отвечает за какие функции?

Все в fs. * Использует пул потоков uv (если они не синхронизированы). Блокирующие вызовы выполняются потоками и по завершении передаются обратно в цикл событий. Мы не можем напрямую «ждать» их в Epoll, но можем передать их через трубку. Pipe имеет 2 конца: один - это поток, и когда он завершен, он записывает данные в канал, другой конец ожидает в цикле Epoll, и когда он получает данные, цикл Epoll просыпается. Итак, Epoll очень хорошо разбирается в трубках.

Основные функции и API-интерфейсы, отвечающие за них, приведены ниже:

EPOOL, KQUEUE, ASYNC и т. Д. В зависимости от ОС

  • Серверы и клиенты TCP / UDP
  • трубы
  • dns.resolve

NGINX

  • сигналы nginx (sigterm)
  • Дочерние процессы (exec, spawn)
  • Вход TTY (консоль)

РЕЗЬБОВЫЙ БАССЕЙН

  • fs.
  • dns.lookup

А цикл обработки событий заботится об отправке и получении результатов, поэтому это своего рода центральная диспетчеризация, которая направляет запросы в C ++ API и результаты обратно в JavaScript, как директор.

Цикл событий

Итак, что такое цикл событий? Это бесконечный цикл while, вызывающий Epoll (kqueue) «ожидание» или «пул», когда что-то интересное (обратный вызов, событие, fs) происходит с Node.js; он направляет его в Node.js и завершает работу, когда в Epoll нечего ждать. Вот как асинхронные вещи работают в Node.js, и почему мы называем это управляемым событиями. Цикл событий - это то, что позволяет Node.js выполнять неблокирующие операции ввода-вывода. Несмотря на то, что JavaScript является однопоточным, по возможности выгружая операции в ядро ​​системы.

Одна итерация цикла обработки событий Node.js называется Tick и имеет свои фазы.

Подробнее о фазах цикла событий, таймерах и process.nextTick () в документации по Node.js (на случай, если вам нужно прочитать об этом):

https://nodejs.org/es/docs/guides/event-loop-timers-and-nexttick/

С момента выпуска Node.js v10.5.0 доступен новый модуль worker_threads.

Модуль worker_threads позволяет использовать потоки, которые выполняют JavaScript параллельно. Чтобы получить к нему доступ:

Рабочие (потоки) полезны для выполнения операций JavaScript с интенсивным использованием ЦП. Они не сильно помогут при работе с интенсивным вводом-выводом. Встроенные в Node.js операции асинхронного ввода-вывода более эффективны, чем могут быть Workers.

В отличие от child_process или кластера, worker_threads может совместно использовать память. Они делают это путем передачи экземпляров ArrayBuffer или совместного использования экземпляров SharedArrayBuffer.

Подробнее о рабочих потоках в документации по Node.js:

https://nodejs.org/api/worker_threads.html

Если вы зашли так далеко, прочитав всю статью, поздравляем 😃 Вы молодец.

❤ Спасибо, что прочитали, и, если этот пост был полезен, пожалуйста, хлопайте в ладоши! И не забудьте ознакомиться с другими моими статьями, следующая будет о шардинге mongodb. Удачи!