Узел — асинхронный ввод-вывод и цикл событий

Node.js

предисловие

Асинхронного ввода-вывода нельзя избежать при изучении Node.Асинхронный ввод-вывод тесно связан с циклом событий, и я не очень хорошо разбирался и разбирался в этой части.Когда я недавно работал над проектом, у меня были некоторые мысли и я их записал.Знания в этой части четко разобраны, если есть какие-либо ошибки, пожалуйста, дайте указатели и распылите~~

некоторые понятия

Синхронный Асинхронный и блокирующий Неблокирующий

Когда я проверил данные, я обнаружил, что многие люди были правы.异步和非阻塞Концепция немного сбивает с толку, на самом деле они совершенно разные, синхронные и асинхронные относятся к行为即两者之间的关系, в то время как блокирующий неблокирующий относится к状态即某一方.

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

$.ajax(url).succedd(() => {
    ......
    // to do something
})

同步异步
Если он синхронный, то после того, как клиент инициирует запрос, он должен дождаться, пока сервер обработает запрос, прежде чем вернуться, чтобы продолжить выполнение последующей логики, поэтомуclient和serve之间就保持了同步的状态.

Если он асинхронный, то он должен быть после того, как клиент инициирует запрос,立即返回, и запрос, возможно, не достиг серверной стороны или запрос обрабатывается.Конечно, в асинхронном случае клиентская сторона обычно регистрирует событие для обработки ситуации после завершения запроса, например успешная функция выше.

阻塞非阻塞
Прежде всего, вам нужно понять концепцию: Js — это один поток, а браузер — нет, ведь ваш запрос выполняется в другом потоке браузера.

Если он блокирует, то该线程就会一直等到这个请求完成之后才能被释放用于其他请求.

Если он неблокирующий, то该线程就可以发起请求后而不用等请求完成继续做其他事情.

总结
Путаница часто возникает из-за того, что неясно, какая часть обсуждения обсуждается (описано ниже), поэтому同步异步讨论的对象是双方, 而阻塞非阻塞讨论的对象是自身.

ввод-вывод и ЦП

Io和Cpu是可以同时进行工作的.

IO:

Ввод/вывод (англ. Input/Output), то есть ввод/вывод, обычно относится к вводу и выводу данных между внутренней памятью и внешней памятью или другими периферийными устройствами.

cpu

Интерпретировать компьютерные инструкции и обрабатывать данные в компьютерном программном обеспечении.

Модель асинхронного ввода-вывода в узле

ИО делится на磁盘IO和网络IO, который имеет два шага

  1. Ожидание готовности данных
  2. Копирование данных из ядра в процесс

Диск Io в узле

Следующее обсуждение основано на системах *nix.
Идеальный асинхронный ввод-вывод должен быть таким, как обсуждалось выше, как показано на рисунке:

На самом деле наша система не может идеально реализовать такой метод вызова.Асинхронный ввод-вывод узла, такой как чтение файлов, реализуется пулом потоков.Как видите, Node использует другой поток для выполнения операции ввода-вывода, уведомляя основной поток после завершения:

И под окном, это использоватьIOCPС точки зрения пользователя, IOCP действительно является идеальным методом асинхронного вызова, но на самом деле он использует пул потоков в ядре.Отличие от nix-системы в том, что пул потоков последней предоставляется пользовательским уровнем.

Сетевой ввод-вывод в узле

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

阻塞 I/O(blocking IO)

Следовательно, характеристика блокирующего ввода-вывода заключается в том, что он блокируется на обоих этапах выполнения ввода-вывода.

非阻塞 I/O(nonblocking IO)

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

I/O 多路复用( IO multiplexing)

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

异步 I/O(asynchronous IO)

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

В узле принят режим мультиплексирования ввода-вывода, а в режиме мультиплексирования ввода-вывода существует несколько подрежимов, таких как чтение, выбор, опрос, epoll и т. д. Node использует наиболее оптимальный режим epoll, вот краткое описание различий и почему epoll оптимален.

read
читать. Это один из самых примитивных и наименее производительных, и он многократно проверяет состояние ввода-вывода, чтобы завершить полное чтение данных. ЦП затрачивается на повторную проверку состояния ввода-вывода, пока не будут доступны окончательные данные. Рисунок 1 представляет собой схематическую диаграмму опроса чтением.

select
Выбрать. Это улучшенная схема, основанная на чтении путем оценки состояния события в файловом дескрипторе. Рисунок 2 представляет собой схематическую диаграмму опроса по выбору. Опрос select имеет более слабое ограничение, потому что он использует массив длиной 1024 для хранения состояния, что означает, что он может проверять до 1024 файловых дескрипторов одновременно.

poll
опрос. Poll — это улучшение по сравнению с select: он использует связанный список, чтобы избежать ограничения длины массива, во-вторых, позволяет избежать ненужных проверок. Но когда файловых дескрипторов много, его производительность очень низкая.

epoll
Эта схема является наиболее эффективным механизмом уведомления о событиях ввода-вывода в Linux.Если при входе в опрос не обнаружено никаких событий ввода-вывода, он будет бездействовать до тех пор, пока не произойдет событие, которое его разбудит. Он фактически использует уведомление о событии и выполняет метод обратного вызова вместо обхода запроса, поэтому он не будет тратить ресурсы ЦП, а эффективность выполнения будет высокой.

В дополнение к этому, альтернативный опрос и выбор имеют следующие недостатки (цитата изстатья):

  1. Каждый раз, когда вы вызываете select, вам нужно копировать набор fd из пользовательского режима в режим ядра.Эти накладные расходы будут очень большими, когда имеется много fd.
  2. В то же время каждый вызов select должен проходить через все fd, переданные в ядре.Эти накладные расходы также очень велики, когда имеется много fd.
  3. Количество файловых дескрипторов, поддерживаемых select, слишком мало, по умолчанию 1024.

улучшения epoll для вышеперечисленного

Поскольку epoll является улучшением выбора и опроса, он должен быть в состоянии избежать трех вышеупомянутых недостатков. Как решается epoll? Перед этим давайте взглянем на разницу в вызывающем интерфейсе между epoll и select и poll.И select, и poll предоставляют только одну функцию — функцию select или poll. И epoll предоставляет три функции: epoll_create, epoll_ctl и epoll_wait, epoll_create — для создания дескриптора epoll, epoll_ctl — для регистрации типа события, которое нужно отслеживать, epoll_wait — для ожидания генерации события.    Решение проблемы первого недостатка epoll находится в функции epoll_ctl. Каждый раз, когда в дескрипторе epoll регистрируется новое событие (укажите EPOLL_CTL_ADD в epoll_ctl), все fd будут копироваться в ядро, а не повторяться во время epoll_wait. epoll гарантирует, что каждый fd будет скопирован только один раз за весь процесс. Что касается второго недостатка, то решение epoll не похоже на select или poll, которые каждый раз добавляют ток в очередь ожидания устройства, соответствующую fd, а только один раз вешают ток во время epoll_ctl (это время существенно) и предусматривают каждый Каждый fd указывает функцию обратного вызова.Когда устройство будет готово и разбудит ожидающих в очереди ожидания, будет вызвана эта функция обратного вызова, и эта функция обратного вызова добавит готовый fd в список готовых). Задача epoll_wait на самом деле состоит в том, чтобы проверить, есть ли готовый fd в этом готовом списке (используйте schedule_timeout(), чтобы некоторое время приостановить работу и какое-то время оценить эффект, что аналогично шагу 7 в реализации select). Что касается третьего недостатка, у epoll нет этого ограничения.Верхний предел поддерживаемого FD — это максимальное количество открытых файлов.Это число, как правило, намного больше, чем 2048. Например, оно составляет около 100 000 на машине с 1 ГБ памяти. Говорят, что это число во многом связано с системной памятью.

Узел в асинхронной сети IO - это использование EPOLL для достижения, в простых терминах, состоит в том, чтобы использовать нить для управления большим количеством запросов IO для связи сообщений через механизм события.

цикл событий

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

Что касается цикла событий, это означает, что JS будет проверять, пуст ли стек выполнения после каждого выполнения задачи синхронизации, и если это так, он будет выполнять зарегистрированный список событий и непрерывно зацикливать процесс. Цикл событий в Node состоит из шести фаз:

Каждый из этих этапов обрабатывает связанные события:

  • таймеры: выполнение обратных вызовов, срок действия которых истекает через setTimeout и setInterval.
  • ожидающий обратный вызов: выполнение обратного вызова ввода-вывода отложено до следующей итерации цикла.
  • бездействие, подготовка: используется только внутри системы.
  • poll: извлекает новые события ввода-вывода; выполняет обратные вызовы, связанные с вводом-выводом (почти во всех случаях, за исключением обратных вызовов выключения, которые запланированы таймерами и setImmediate() ), в остальных узлах блокируется здесь. (то есть содержание этой статьи связано))
  • проверка: здесь выполняется функция обратного вызова setImmediate().
  • обратные вызовы закрытия: выполнение обратного вызова события закрытия, например socket.on('close'[fn]) или http.server.on('close, fn).

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

fs.open = function(path, flags, mode, callback){

//...

binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback);

}

Функция fs.open() заключается в открытии файла по указанному пути и параметрам для получения дескриптора файла, что является начальной операцией всех последующих операций ввода-вывода. Как видно из предыдущего кода, код на уровне JavaScript выполняет низкоуровневые операции, вызывая модуль ядра C++.

Основной модуль Node вызывается из JavaScript, основной модуль вызывает встроенный модуль C++, а встроенный модуль выполняет системные вызовы через libuv, что является классическим методом вызова в Node. Здесь libuv используется как уровень инкапсуляции, и есть две реализации платформы, которые по существу вызывают метод uv_fs_open(). Во время вызова uv_fs_open() мы создаем объект запроса FSReqWrap. Параметры и текущие методы, переданные из уровня JavaScript, инкапсулированы в этот объект запроса, а функция обратного вызова, о которой мы больше всего беспокоимся, установлена ​​в атрибуте oncomplete_sym этого объекта:

req_wrap->object_->Set(oncomplete_sym, callback);

Метод QueueUserWorkItem() принимает 3 параметра: первый параметр — это ссылка на выполняемый метод, здесь указан uv_fs_thread_proc; второй параметр — это параметр, необходимый для запуска метода uv_fs_thread_proc; третий параметр — флаг выполнения. Когда в пуле потоков есть доступные потоки, мы вызываем метод uv_fs_thread_proc(). Метод uv_fs_thread_proc() будет вызывать соответствующую базовую функцию в соответствии с типом входящего параметра. Взяв в качестве примера uv_fs_open(), фактически вызывается метод fs_open().

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

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


График взят из Simple NodeJs

До сих пор весь асинхронный процесс ввода-вывода в Node был ясен: это механизм управления, основанный на пуле потоков ввода-вывода\epoll, цикле событий и объекте запроса.

Почему Node лучше подходит для интенсивного ввода-вывода

О чем говорит Node, так это о том, что он больше подходит дляIO密集型система и имеет更好的性能, Это на самом деле тесно связано с его асинхронным вводом-выводом.

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

Фундаментальная причина заключается в том, что синхронная блокировка ввода-вывода должна быть每一个请求创建一个线程, В момент Io поток блокируется, хоть и не потребляет ЦП, но имеет накладные расходы на память,当大并发的请求到来时, 内存很快被用光, 导致服务器缓慢, добавление,切换上下文代价也会消耗cpu资源. А асинхронный Io Node обрабатывается через механизм событий, ему не нужно создавать поток для каждого запроса, поэтому производительность Node выше.

Особенно в ситуации с интенсивным вводом-выводом, такой как Интернет, это более выгодно.Помимо Node, на самом деле существует еще один сервер механизма событий Ngnix.Если вы понимаете механизм Node, Ngnix должно быть легко понять, если вам интересно Рекомендуем посмотретьэта статья.

Суммировать

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

Предполагая, что ваш бизнес интенсивно использует ЦП, вам определенно не подходит использование Node для разработки. Почему не подходит? Поскольку Node является однопоточным, когда вы заблокированы в вычислениях, другие события не могут быть выполнены, запросы не могут быть обработаны, а обратные вызовы не могут быть обработаны.

Итак, в интенсивном вводе-выводе Node лучше, чем Java? Не обязательно, но все же зависит от вашего бизнеса. Если ваш бизнес очень параллелен, но ресурсы вашего сервера ограничены, это похоже на вход сейчас, Node может вводить 10 человек за раз, а Java ставит в очередь одного человека по очереди, если 10 человек входят одновременно, из конечно, Node выгоднее, но если будет 100 человек (типа 1w асинхронных запросов и т.д.), то Node приведет к тому, что приложение будет приостановлено из-за его асинхронного механизма, память будет париться, IO будет заблокирован, и он не будет восстановлен.В настоящее время вы можете только перезапустить. Java, с другой стороны, может обрабатывать по порядку, хотя и немного медленнее. А потери от онлайн-аварии, вызванной зависанием сервера, неизмеримы. (Конечно, Node справится и с этим, если ресурсов сервера достаточно).

Наконец, по сути, Java тоже является библиотекой с асинхронным вводом-выводом, но условно говоря, синтаксис Node более естественный и близкий, и он больше подходит.

Ссылки и цитаты

Как понять разницу между блокирующим неблокирующим и синхронным асинхронным?
Linux epoll и Node.js Event Loop и мультиплексирование ввода/вывода
В чем суть высокой параллелизма и высокой производительности приложений node.js?
Подробное объяснение режима ввода-вывода Linux и выбор, опрос, epoll
Почему асинхронный ввод-вывод работает лучше, чем синхронный блокирующий ввод-вывод?
Подробное объяснение Nodejs