Эта функция доступна на уровне ОС и называется (как ни странно) асинхронным вводом / выводом или неблокирующим вводом / выводом (Windows также вызывает / называет его перекрытым вводом / выводом).
На самом низком уровне, в C (C # / Swift), операционная система предоставляет API для отслеживания запросов и ответов. Существуют различные API-интерфейсы, доступные в зависимости от вашей ОС, и Node.js использует libuv для автоматического выбора лучшего доступного API во время компиляции, но для понимания того, как работает асинхронный API, давайте посмотрим на API который доступен для всех платформ: системный вызов select()
.
Функция select()
выглядит примерно так:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, time *timeout);
Данные fd_set
Структура - это набор / список файловых дескрипторов, в которых вы хотите наблюдать на предмет активности ввода-вывода. И помните, что в сокетах POSIX также есть файловые дескрипторы. Вы используете этот API следующим образом:
// Pseudocode:
// Say you just sent a request to a mysql database and also sent a http
// request to google maps. You are waiting for data to come from both.
// Instead of calling `read()` which would block the thread you add
// the sockets to the read set:
add mysql_socket to readfds
add maps_socket to readfds
// Now you have nothing else to do so you are free to wait for network
// I/O. Great, call select:
select(2, &readfds, NULL, NULL, NULL);
// Select is a blocking call. Yes, non-blocking I/O involves calling a
// blocking function. Yes it sounds ironic but the main difference is
// that we are not blocking waiting for each individual I/O activity,
// we are waiting for ALL of them
// At some point select returns. This is where we check which request
// matches the response:
check readfds if mysql_socket is set {
then call mysql_handler_callback()
}
check readfds if maps_socket is set {
then call maps_handler_callback()
}
go to beginning of loop
Итак, в основном ответ на ваш вопрос состоит в том, что мы проверяем структуру данных, какой сокет / файл только что инициировал операцию ввода-вывода, и выполняем соответствующий код.
Вы, без сомнения, легко заметите, как обобщить этот шаблон кода: вместо того, чтобы вручную настраивать и проверять файловые дескрипторы, вы можете сохранить все ожидающие асинхронные c запросы и обратные вызовы в списке или массиве и l oop через это до и после select()
. Фактически, это то, что делает Node.js (и javascript в целом). И именно этот список обратных вызовов / файловых дескрипторов иногда называют очередью событий - это не очередь как таковая, а просто набор вещей, которые вы ожидаете выполнения.
Функция select()
также имеет параметр тайм-аута в конце, который можно использовать для реализации setTimeout()
и setInterval()
и в браузерах обработки событий GUI, чтобы мы могли запускать код в ожидании ввода-вывода. Помните, что select
блокирует - мы можем запустить другой код, только если select вернет. При тщательном управлении таймерами мы можем вычислить соответствующее значение для передачи в качестве тайм-аута select
.
Структура данных fd_set
на самом деле не является связанным списком. В более старых реализациях это битовое поле. Более современная реализация может улучшить битовое поле, если оно соответствует API. Но это частично объясняет, почему существует так много конкурирующих асинхронных c API, таких как poll
, epoll
, kqueue
et c. Они были созданы, чтобы преодолеть ограничения select
. Различные API-интерфейсы по-разному отслеживают файловые дескрипторы, некоторые используют связанные списки, некоторые используют таблицы ha sh, некоторые обеспечивают масштабируемость (возможность прослушивать десятки тысяч сокетов), а некоторые обеспечивают скорость, и большинство из них стараются сделать и то, и другое лучше чем другие. Что бы они ни использовали, в конечном итоге то, что используется для хранения запроса, является просто структурой данных, которая отслеживает файловые дескрипторы .