Как работает однопоточная неблокирующая модель ввода-вывода в Node.js


325

Я не программист Node, но мне интересно, как работает однопоточная неблокирующая модель IO . После того, как я прочитал статью « понимание узла-js-события-цикла» , я действительно запутался в этом. Это дало пример для модели:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

Que: Когда есть два запроса A (идет первым) и B, поскольку существует только один поток, программа на стороне сервера будет обрабатывать запрос A в первую очередь: выполнение SQL-запросов - это спящий оператор, означающий ожидание ввода-вывода. И программа застряла в I/Oожидании и не может выполнить код, который отображает веб-страницу позади. Будет ли программа переключаться на запрос B во время ожидания? На мой взгляд, из-за однопотоковой модели невозможно переключить один запрос с другого. Но заголовок примера кода говорит, что все работает параллельно, кроме вашего кода .

(PS Я не уверен, что неправильно понял код или нет, так как никогда не использовал Node.) Как Node переключается на B во время ожидания? И можете ли вы объяснить однопоточную неблокирующую модель IO узла простым способом? Буду признателен, если вы поможете мне. :)

Ответы:


374

Node.js построен на libuv , кроссплатформенной библиотеке, которая абстрагирует apis / syscalls для асинхронного (неблокирующего) ввода / вывода, предоставляемого поддерживаемыми ОС (по крайней мере, Unix, OS X и Windows).

Асинхронный ввод-вывод

В этой модели программирования операции открытия / чтения / записи на устройствах и ресурсах (сокеты, файловая система и т. Д.), Управляемых файловой системой , не блокируют вызывающий поток (как в типичной синхронной c-подобной модели) и просто отмечают процесс (в структуре данных уровня ядра / ОС), чтобы получать уведомления о появлении новых данных или событий. В случае приложения, подобного веб-серверу, процесс затем должен выяснить, к какому запросу / контексту относится уведомленное событие, и продолжить обработку запроса оттуда. Обратите внимание, что это обязательно будет означать, что вы будете находиться в другом стековом фрейме, чем тот, который отправил запрос к ОС, поскольку последняя должна была уступить диспетчеру процесса, чтобы однопоточный процесс обрабатывал новые события.

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

Модель узла (Стиль прохождения продолжения и Цикл событий)

Node решает проблему, используя возможности языка javascript, чтобы сделать эту модель более синхронной, побуждая программиста использовать определенный стиль программирования. Каждая функция, которая запрашивает ввод-вывод, имеет такую ​​же сигнатуру, function (... parameters ..., callback)и ей нужно дать обратный вызов, который будет вызван, когда запрошенная операция будет завершена (имейте в виду, что большую часть времени тратится на ожидание ОС, чтобы сообщить о завершении - время, которое может быть потратил на другую работу). Поддержка Javascript для замыканий позволяет вам использовать переменные, которые вы определили во внешней (вызывающей) функции внутри тела обратного вызова - это позволяет сохранять состояние между различными функциями, которые будут вызываться средой выполнения узла независимо. Смотрите также продолжение прохождения стиля .

Более того, после вызова функции, порождающей операцию ввода-вывода, вызывающая функция обычно returnуправляет циклом событий узла . Этот цикл вызовет следующий обратный вызов или функцию, которая была запланирована для выполнения (скорее всего, потому что соответствующее событие было уведомлено ОС) - это позволяет одновременно обрабатывать несколько запросов.

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

Сильно параллельный, без параллелизма

И последнее замечание: фраза «все работает параллельно, кроме вашего кода», отлично справляется с заданием точки, позволяющей вашему коду одновременно обрабатывать запросы из сотен тысяч открытых сокетов в одном потоке путем мультиплексирования и упорядочения всех ваших js. логика в одном потоке выполнения (даже если сказать, что «все работает параллельно», вероятно, здесь не правильно - см. Параллелизм против параллелизма - в чем разница? ). Это работает очень хорошо для серверов веб-приложений, так как большую часть времени фактически тратится на ожидание сети или диска (базы данных / сокетов), и логика на самом деле не сильно загружает ЦП - то есть: это хорошо работает для рабочих нагрузок, связанных с вводом-выводом .


45
Последующие вопросы: как в действительности происходит I / O? Node отправляет запрос в систему и запрашивает уведомление о его завершении. Таким образом, система выполняет поток, который выполняет ввод-вывод, или система также выполняет асинхронный ввод-вывод на аппаратном уровне с использованием прерываний? Что-то где-то должно ждать завершения ввода-вывода, и это будет блокировать, пока это не будет сделано, и потреблять некоторое количество ресурсов.
Филипп

6
Просто заметил, что на этот комментарий есть ответ @ user568109 ниже, я хотел бы, чтобы был способ объединить эти два ответа.
Ифалин

4
Я хотел бы, чтобы вы могли написать ответ вдвое дольше, чтобы я понял в два раза лучше.
Рафаэль Эйнг

Узел поддерживается во многих местах, для записи. Когда я разрабатывал прошивку для маршрутизаторов MIPS32, на них можно было запускать Node.JS через OpenWRT.
Qix - МОНИКА БЫЛА ПЛОЩАДЬ

Как это оценка по апачу? Apache также способен обрабатывать параллельные соединения с отдельным потоком.
Сухаил Гупта

210

Ну, чтобы дать некоторую перспективу, позвольте мне сравнить node.js с apache.

Apache - это многопоточный HTTP-сервер, для каждого запроса, который получает сервер, он создает отдельный поток, который обрабатывает этот запрос.

Node.js, с другой стороны, управляется событиями, обрабатывая все запросы асинхронно из одного потока.

Когда A и B получены на Apache, создаются два потока, которые обрабатывают запросы. Каждый обрабатывает запрос в отдельности, каждый ожидает результатов запроса перед обслуживанием страницы. Страница обслуживается только до завершения запроса. Выборка запроса блокируется, поскольку сервер не может выполнить остальную часть потока, пока не получит результат.

В узле c.query обрабатывается асинхронно, что означает, что в то время как c.query извлекает результаты для A, он переходит к обработке c.query для B, а когда результаты поступают для A, он отправляет результаты обратно в callback, который отправляет ответ. Node.js знает, как выполнить обратный вызов, когда выборка завершится.

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

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

редактировать

Запрос SQL взят из библиотеки mysql . Он реализует стиль обратного вызова, а также источник событий для очереди SQL-запросов. Он не выполняет их асинхронно, что делается внутренними потоками libuv, которые обеспечивают абстракцию неблокирующего ввода / вывода. Для выполнения запроса выполняются следующие шаги:

  1. Откройте соединение с БД, само соединение может быть установлено асинхронно.
  2. Как только БД подключен, запрос передается на сервер. Запросы могут быть поставлены в очередь.
  3. Цикл основного события получает уведомление о завершении с обратным вызовом или событием.
  4. Основной цикл выполняет ваш обратный вызов / обработчик событий.

Входящие запросы на http-сервер обрабатываются аналогичным образом. Архитектура внутреннего потока выглядит примерно так:

цикл событий node.js

Потоки C ++ - это libuv-потоки, которые выполняют асинхронный ввод-вывод (диск или сеть). Цикл основного события продолжает выполняться после отправки запроса в пул потоков. Он может принимать больше запросов, поскольку не ждет и не спит. SQL-запросы / HTTP-запросы / чтение файловой системы - все происходит так.


16
Диаграмма очень полезна.
Anmol Saraf

14
Подождите, так что на вашей диаграмме у вас есть "внутренний пул потоков C ++", что означает, что все операции блокировки ввода-вывода будут порождать поток, верно? Так что, если мое приложение Node выполняет какой-либо ввод-вывод для каждого запроса , нет ли разницы между моделью Node и моделью Apache? Я не получаю эту часть извините.
gav.newalkar

21
@ gav.newalkar Они не создают поток, запросы ставятся в очередь. Потоки в пуле потоков обрабатывают их. Потоки не являются динамическими и для каждого запроса, как в Apache. Они обычно являются фиксированными и отличаются от системы к системе.
user568109

10
@ user568109 Но Apache также использует пул потоков ( httpd.apache.org/docs/2.4/mod/worker.html ). Так что, в конце концов, разница между установкой с node.js отличается от установки с Apache впереди только тем, где расположен пул потоков, не так ли?
Крис

13
Эта диаграмма должна быть на первой странице официальных документов.
Bouvierr

52

Node.js использует libuv за кулисами. В libuv есть пул потоков (по умолчанию размером 4). Поэтому Node.js использует потоки для достижения параллелизма.

Тем не менее , ваш код работает на одном потоке (т.е. все обратных вызовов функций Node.js будет вызываться в том же потоке, так называемая петля-нить или событие петля). Когда люди говорят «Node.js работает в одном потоке», они действительно говорят «обратные вызовы Node.js работают в одном потоке».


1
Короткий, но
четкий

1
хороший ответ я бы добавил, что ввод / вывод происходит вне этого основного цикла обработки событий, цикла обработки потока, потока запроса
Ionut Popa

это ответ, который я искал за 2 часа, как управлялся параллелизмом в однопоточном приложении
Мухаммед Рамзан

да, трудно получить ответ «следующего уровня». Это объясняет, где на самом деле выполняется IO (в пуле потоков где-то еще)
Оливер Шоу,

9

Node.js основан на модели программирования цикла событий. Цикл обработки событий выполняется в одном потоке и многократно ожидает события, а затем запускает все обработчики событий, подписанные на эти события. События могут быть, например,

  • ожидание по таймеру завершено
  • следующий кусок данных готов к записи в этот файл
  • Theres новый HTTP-запрос приходит наш путь

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

(Под капотом есть немного магии относительно того, откуда происходят события. Некоторые из них включают в себя рабочие потоки низкого уровня, работающие параллельно.)

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

просмотр высокого уровня цикла событий

Согласно: «Цикл событий от 10 000 футов - основная концепция Node.js» .


5

Функция c.query () имеет два аргумента

c.query("Fetch Data", "Post-Processing of Data")

Операция «Извлечение данных» в данном случае является запросом к DB, теперь это может быть обработано Node.js путем создания рабочего потока и предоставления ему этой задачи выполнения запроса к DB. (Помните, что Node.js может создавать потоки внутри). Это позволяет функции мгновенно вернуться без каких-либо задержек

Второй аргумент «Постобработка данных» является функцией обратного вызова, структура узла регистрирует этот обратный вызов и вызывается циклом обработки событий.

Таким образом, оператор c.query (paramenter1, parameter2)будет возвращаться мгновенно, позволяя узлу обслуживать другой запрос.

PS: я только начал понимать узел, на самом деле я хотел написать это как комментарий к @Philip, но так как у меня не было достаточно очков репутации, поэтому написал это как ответ.


3

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

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

в вашем примере: есть два запроса A (идет первым) и B. Вы выполняете запрос A, ваш код продолжает выполняться синхронно и выполняет запрос B. Цикл обработки событий обрабатывает запрос A, когда он завершается, он вызывает обратный вызов запроса A с результат, то же самое относится к запросу Б.


3
«Конечно, на бэкэнде есть потоки и процессы для доступа к БД и выполнения процессов. Однако они не подвергаются явному воздействию вашего кода» - если я беру из этой фразы, то я не вижу никакой разницы между тем, что Node do или любой многопоточный фреймворк - скажем, Java Framework Spring - делает. Есть темы, но вы не контролируете их создание.
Рафаэль Эйнг

@RafaelEyng Я думаю, что для обработки серии нескольких запросов узел всегда будет иметь для этого один поток. Я не уверен, что каждый обратный вызов помещается в новый экземпляр потоков помимо других процессов, таких как доступ к базе данных, но, по крайней мере, мы, конечно, знаем, что узел не создает экземпляры потоков каждый раз, когда он получает запрос, который должен будет ждать в очереди перед обработкой (выполнением до обратный звонок).
Холодный Цербер

1

Хорошо, большинство вещей должно быть ясно до сих пор ... сложная часть - это SQL : если он на самом деле не выполняется в другом потоке или процессе полностью, SQL-выполнение должно быть разбито на отдельные шаги ( SQL-процессор предназначен для асинхронного выполнения!), Где выполняются неблокирующие, а блокирующие (например, спящий режим) могут фактически передаваться в ядро ​​(как аварийное прерывание / событие) и помещаться в список событий для основной цикл.

Это означает, что, например, интерпретация SQL и т. Д. Выполняется немедленно, но во время ожидания (сохраняется как событие, которое должно произойти в будущем ядром в некоторой структуре kqueue, epoll, ...; вместе с другими операциями ввода-вывода). ) основной цикл может выполнять другие действия и в конечном итоге проверять, произошло ли что-то из этих операций ввода-вывода и ожидает.

Итак, перефразируя это снова: программа никогда (не может застрять), спящие вызовы никогда не выполняются. Их обязанность выполняется ядром (что-то написать, ждать, пока что-то придет по сети, ждать, пока пройдет время) или другим потоком или процессом. - Процесс Node проверяет, выполнено ли по крайней мере одно из этих заданий ядром при единственном блокирующем вызове ОС один раз в каждом цикле цикла обработки событий. Эта точка достигается, когда все неблокирование сделано.

Очистить? :-)

Я не знаю Node. Но откуда взялся c.query?


kqueue epoll для масштабируемого уведомления асинхронного ввода-вывода в ядре Linux. У узла есть libuv для этого. Узел целиком находится в пользовательском пространстве. Это не зависит от того, какое ядро ​​реализует.
user568109

1
@ user568109, libuv - посредник Node. Любая асинхронная структура зависит (напрямую или нет) от некоторой поддержки асинхронного ввода-вывода в ядре. Так?
Роберт Симер

Извините за путаницу. Операции с сокетом требуют неблокирующего ввода-вывода из ядра. Он заботится об асинхронной обработке. Но асинхронный файловый ввод / вывод обрабатывается самой libuv. Ваш ответ не говорит об этом. Он обрабатывает оба одинаково, будучи обработанным ядром.
user568109
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.