Эта статья была впервые опубликована на github, если вам интересно, пожалуйстакликните сюда
Nodejs выполняется в одном потоке, и в то же время он основан на модели программирования неблокирующего ввода-вывода, управляемой событиями. Это позволяет нам продолжить выполнение кода, не дожидаясь возврата результата асинхронной операции. Когда запускается асинхронное событие, основной поток уведомляется, и основной поток выполняет обратный вызов соответствующего события.
Вышеизложенное хорошо известно. Сегодня мы начнем с исходного кода и проанализируем механизм цикла обработки событий nodejs.
архитектура nodejs
Во-первых, давайте посмотрим на архитектуру Nodejs, как показано на следующем рисунке:
Как показано на рисунке выше, NodeJs делится на- Код пользователя (js-код)
Пользовательский код — это код приложения, которое мы пишем, пакет npm, модуль js, встроенный в nodejs, и т. д. Большая часть нашей повседневной работы — это написание кода на этом уровне.
- обязательный кодилиСторонний плагин (код js или C/C++)
Клейкий код, что позволяет JS вызовов код C / C ++. Он может быть понят как мост, конец моста является JS, а конец моста представляет собой C / C ++. Через этот мост JS может вызывать C / C ++.
В nodejs основная функция связующего кода заключается в предоставлении библиотеки C/C++, реализованной в нижней части nodejs, в среду js.
сторонний плагинЭто библиотека C/C++, реализованная нами, и нам нужно самим реализовать связующий код, чтобы соединить js и C/C++.
- базовая библиотека
Зависимые библиотеки nodejs, в том числе знаменитые V8 и libuv.
V8: Мы все знаем, что это набор эффективной среды выполнения javascript, разработанной Google, и основная причина, по которой nodejs может эффективно выполнять код js, в основном заключается в нем.
libuv: Библиотека асинхронных функций представляет собой набор языков C, эффективная модель асинхронного программирования nodejs во многом обязана достижению libuv, и libuv сегодня находится в центре нашего внимания для анализа.
Есть также некоторые другие зависимые библиотеки
http-parser: отвечает за разбор http-ответа
openssl: шифрование и дешифрование
c-ares: разрешение DNS
npm: менеджер пакетов nodejs
...
Мы не будем внести слишком много о Nodejs. Вы можете прочитать и учиться самостоятельно. Далее мы сосредоточимся на Libuv.
либувская архитектура
Мы знаем, что ядром асинхронного механизма nodejs является libuv. libuv отвечает за коммуникационный мост между nodejs и асинхронными задачами, такими как файлы и сети. Следующая картинка дает нам общее представление о libuv:
Это изображение официального сайта libuv.Очевидно, что сетевой ввод-вывод nodejs, файловый ввод-вывод, операции DNS и некоторый пользовательский код работают в libuv. Поскольку мы говорили об асинхронности, давайте сначала суммируем асинхронные события в nodejs:
- Без ввода/вывода:
- таймер (setTimeout, setInterval)
- микрозадача (обещание)
- process.nextTick
- setImmediate
- DNS.lookup
- Ввод/вывод:
- Сетевой ввод-вывод
- файловый ввод/вывод
- некоторые операции DNS
- ...
Сетевой ввод-вывод
Для сетевого ввода-вывода механизм реализации каждой платформы отличается: Linux — это модель epoll, Unix-подобная — kquene, Windows — эффективный порт завершения IOCP, SunOs — порт событий, а libuv выполняет эти сетевые операции ввода-вывода. О модели пакет.
Файловый ввод-вывод, асинхронные операции DNS
libuv также поддерживает пул из 4 потоков по умолчанию, эти потоки отвечают за выполнение файловых операций ввода-вывода, операций DNS и пользовательского асинхронного кода. Когда уровень js передает операционную задачу в libuv, libuv добавит задачу в очередь. Тогда есть два случая:
- 1. Когда все потоки в пуле потоков заняты, задачи в очереди будут помещены в очередь для незанятых потоков.
- 2. Когда в пуле потоков есть доступные потоки, задача выводится из очереди на выполнение, после завершения выполнения поток возвращается в пул потоков и ожидает следующей задачи. В то же время цикл событий уведомляется в виде события, а цикл событий получает событие и выполняет функцию обратного вызова, зарегистрированную для события.
Конечно, если вы считаете, что 4 потока недостаточно, вы можете установить переменные среды при запуске nodejs.UV_THREADPOOL_SIZEЧтобы настроить, для соображений производительности системы, libuv предусматривает, что количество потоков, которые могут быть установлены, не может превышать128индивидуальный.
исходный код nodejs
Давайте кратко представим процесс запуска nodejs:
- 1. звонокplatformInitметод, инициализироватьnodejsрабочая среда.
- 2, звонитеperformance_node_startметод, даnodejsВедите статистику производительности.
- 3.opensslУстановить суждение.
- 4. Звонокv8_platform.Initialize, инициализацияlibuvПул потоков.
- 5. звонокV8::Initialize, инициализацияV8окружающая обстановка.
- 6, создайтеnodejsЗапустите экземпляр.
- 7. Запустите экземпляр, созданный на предыдущем шаге.
- 8. Запустить выполнение js-файла.После выполнения кода синхронизации войти в цикл обработки событий.
- 9. Уничтожайте, когда нет событий для прослушиванияnodejsПример, программа выполняется.
Выше приведен весь процесс выполнения nodejs файла js. Далее мы сосредоточимся на восьмом шаге, цикле событий.
Давайте посмотрим на несколько ключевых исходных кодов:
- 1,core.c, основной файл, в котором выполняется цикл обработки событий.
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
//判断事件循环是否存活。
r = uv__loop_alive(loop);
//如果没有存活,更新时间戳
if (!r)
uv__update_time(loop);
//如果事件循环存活,并且事件循环没有停止。
while (r != 0 && loop->stop_flag == 0) {
//更新当前时间戳
uv__update_time(loop);
//执行 timers 队列
uv__run_timers(loop);
//执行由于上个循环未执行完,并被延迟到这个循环的I/O 回调。
ran_pending = uv__run_pending(loop);
//内部调用,用户不care,忽略
uv__run_idle(loop);
//内部调用,用户不care,忽略
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
//计算距离下一个timer到来的时间差。
timeout = uv_backend_timeout(loop);
//进入 轮询 阶段,该阶段轮询I/O事件,有则执行,无则阻塞,直到超出timeout的时间。
uv__io_poll(loop, timeout);
//进入check阶段,主要执行 setImmediate 回调。
uv__run_check(loop);
//进行close阶段,主要执行 **关闭** 事件
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//更新当前时间戳
uv__update_time(loop);
//再次执行timers回调。
uv__run_timers(loop);
}
//判断当前事件循环是否存活。
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
- 2,timersСтадия, исходный файл:timers.c.
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
//取出定时器堆中超时时间最近的定时器句柄
heap_node = heap_min((struct heap*) &loop->timer_heap);
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
// 判断最近的一个定时器句柄的超时时间是否大于当前时间,如果大于当前时间,说明还未超时,跳出循环。
if (handle->timeout > loop->time)
break;
// 停止最近的定时器句柄
uv_timer_stop(handle);
// 判断定时器句柄类型是否是repeat类型,如果是,重新创建一个定时器句柄。
uv_timer_again(handle);
//执行定时器句柄绑定的回调函数
handle->timer_cb(handle);
}
}
- 3.этап опросаИсходный код, файл исходного кода:kquene.c
void uv__io_poll(uv_loop_t* loop, int timeout) {
/*一连串的变量初始化*/
//判断是否有事件发生
if (loop->nfds == 0) {
//判断观察者队列是否为空,如果为空,则返回
assert(QUEUE_EMPTY(&loop->watcher_queue));
return;
}
nevents = 0;
// 观察者队列不为空
while (!QUEUE_EMPTY(&loop->watcher_queue)) {
/*
取出队列头的观察者对象
取出观察者对象感兴趣的事件并监听。
*/
....省略一些代码
w->events = w->pevents;
}
assert(timeout >= -1);
//如果有超时时间,将当前时间赋给base变量
base = loop->time;
// 本轮执行监听事件的最大数量
count = 48; /* Benchmarks suggest this gives the best throughput. */
//进入监听循环
for (;; nevents = 0) {
// 有超时时间的话,初始化spec
if (timeout != -1) {
spec.tv_sec = timeout / 1000;
spec.tv_nsec = (timeout % 1000) * 1000000;
}
if (pset != NULL)
pthread_sigmask(SIG_BLOCK, pset, NULL);
// 监听内核事件,当有事件到来时,即返回事件的数量。
// timeout 为监听的超时时间,超时时间一到即返回。
// 我们知道,timeout是传进来得下一个timers到来的时间差,所以,在timeout时间内,event-loop会一直阻塞在此处,直到超时时间到来或者有内核事件触发。
nfds = kevent(loop->backend_fd,
events,
nevents,
events,
ARRAY_SIZE(events),
timeout == -1 ? NULL : &spec);
if (pset != NULL)
pthread_sigmask(SIG_UNBLOCK, pset, NULL);
/* Update loop->time unconditionally. It's tempting to skip the update when
* timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the
* operating system didn't reschedule our process while in the syscall.
*/
SAVE_ERRNO(uv__update_time(loop));
//如果内核没有监听到可用事件,且本次监听有超时时间,则返回。
if (nfds == 0) {
assert(timeout != -1);
return;
}
if (nfds == -1) {
if (errno != EINTR)
abort();
if (timeout == 0)
return;
if (timeout == -1)
continue;
/* Interrupted by a signal. Update timeout and poll again. */
goto update_timeout;
}
。。。
//判断事件循环的观察者队列是否为空
assert(loop->watchers != NULL);
loop->watchers[loop->nwatchers] = (void*) events;
loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
// 循环处理内核返回的事件,执行事件绑定的回调函数
for (i = 0; i < nfds; i++) {
。。。。
}
}
Стадия uv__io_poll имеет самый длинный исходный код и самую сложную логику, ее можно обобщить следующим образом: Когда обратный вызов события, зарегистрированный кодом слоя js, не возвращается, цикл событий блокируется на этапе опроса. Увидев это, вы можете подумать, а не заблокируется ли он здесь навсегда?
1. Прежде всего, при выполнении фазы опроса будет передан тайм-аут, который является максимальным временем блокировки фазы опроса.
2. Во-вторых, на этапе опроса, когда время ожидания не истекло, если событие возвращается, будет выполнена функция обратного вызова, зарегистрированная событием. По истечении тайм-аута выйти из этапа опроса и выполнить следующий этап.
Таким образом, нам не нужно беспокоиться о вечной блокировке цикла обработки событий на этапе опроса.
Это два основных этапа цикла событий. Из-за ограничений по площади,timersДругой исходный код сцены иsetImmediate,process.nextTickИсходный код не будет указан.Заинтересованные дети могут посмотреть исходный код.
Наконец, принцип цикла событий резюмируется следующим образом, вы можете игнорировать вышеизложенное, просто запомните следующее резюме.
принцип цикла событий
- инициализация узла
- Инициализируйте среду узла.
- Выполните введенный код.
- воплощать в жизньprocess.nextTickПерезвоните.
- Выполнять микрозадачи.
- войти в цикл событий
- Войтиtimersсцена
- Проверьте, есть ли в очереди таймера обратные вызовы таймера с истекшим сроком действия, и если да, выполните обратные вызовы таймера с истекшим сроком действия в порядке возрастания timerId.
- Проверить, есть ли задачи process.nextTick, если есть, выполнить их все.
- Проверяем, есть ли микрозадачи, и если да, то выполняем их все.
- выйти из этого этапа.
- ВойтиIO callbacksсцена.
- Проверьте наличие ожидающих обратных вызовов ввода-вывода. Если есть, выполните обратный вызов. Если нет, выйдите из этого этапа.
- Проверить, есть ли задачи process.nextTick, если есть, выполнить их все.
- Проверяем, есть ли микрозадачи, и если да, то выполняем их все.
- выйти из этого этапа.
- Войтибездельничать, готовитьсясцена:
- Эти два этапа имеют мало общего с нашим программированием, поэтому пока не будем их перечислять.
- Войтиpollсцена
- Сначала проверьте, есть ли незаконченный callback, если есть, то тут два случая.
- Первый случай:
- Если есть доступные обратные вызовы (доступные обратные вызовы включают таймеры с истекшим сроком действия, некоторые события ввода-вывода и т. д.), выполните все доступные обратные вызовы.
- Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
- Проверьте наличие микротаков, и если да, то выполните их все.
- выйти из этого этапа.
- Второй случай:
- Если обратный звонок недоступен.
- Проверьте наличие немедленных исправлений, и если это так, этап опроса на выходе. Если нет, то на данном этапе блокировка, ожидание уведомления о новом событии.
- Первый случай:
- Если нет ожидающих обратных вызовов, выйдите из фазы опроса.
- Сначала проверьте, есть ли незаконченный callback, если есть, то тут два случая.
- Войтиcheckсцена.
- Если есть немедленные обратные вызовы, выполняются все немедленные обратные вызовы.
- Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
- Проверьте наличие микротаков, и если да, то выполните их все.
- покидатьcheckсцена
- Войтиclosingсцена.
- Если есть немедленные обратные вызовы, выполняются все немедленные обратные вызовы.
- Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
- Проверьте наличие микротаков, и если да, то выполните их все.
- покидатьclosingсцена
- Проверяем, есть ли активные хендлы (обработчики событий для таймеров, IO и т.д.).
- Если это так, переходите к следующему циклу.
- Если нет, завершите цикл событий и выйдите из программы.
- Войтиtimersсцена
Внимательные детские туфли могут обнаружить, что следующие процессы выполняются по порядку перед выходом из каждой подфазы цикла событий:
- Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
- Проверьте наличие микротаков, и если да, то выполните их все.
- Выйти из текущего этапа.
Помните это правило.
Затем по приведенной выше формуле подставить различные тестовые коды, относящиеся к циклу событий nodejs в интернете, я думаю, вы уже можете объяснить, почему выводится такой результат. Если нет, то пишите мне в личные сообщения~~