JavaScript, как многие из вас, должно быть, слышали, — однопоточный. Это означает, что он может выполнять только одну задачу за раз. Все задачи в JavaScript выполняются в одном потоке, который называется основным потоком.
Node.js — среда выполнения JavaScript, которая позволяет анализировать, компилировать и запускать JavaScript-код. Node делает это с помощью движка с открытым исходным кодом V8 от Google, написанного на C++.
С движком V8 Node может “под капотом», скрытно для пользователя, выполнять как JavaScript, так и C++. Это позволяет писать как синхронный, так и асинхронный JavaScript-код в однопоточной среде, не беспокоясь о потоковой передаче или параллелизме.
Цикл событий
Цикл событий — вот что дает приложениям Node возможность работать в одном потоке, но при этом поддерживает асинхронные операции и неблокирующий ввод-вывод. Для понимания функциональности цикла событий важно знать, что такое стек вызовов, очередь сообщений и API C++.
Стек вызовов — это преимущественно LIFO-стек (Last In, First Out, “последний на вход, первый на выход”), который отслеживает, какая задача будет выполняться следующей в основном потоке. Задачи, определенные в вашем JavaScript-коде, помещаются в этот стек при выполнении кода. Посмотрим, как через стек вызовов выполняется приведенный ниже код:
const bar = () => console.log("bar")
const baz = () => console.log("baz")
const foo = () => {
console.log("foo")
bar()
baz()
}
foo()
Данный код предназначен для простой синхронной программы и поэтому не требует участия API C++ или очереди сообщений.
Каждая из задач в программе помещается в стек вызовов и выполняется в режиме LIFO. Вывод на консоли будет выглядеть так:
foo
bar
baz
При выполнении асинхронной задачи процесс становится сложнее, и именно здесь в игру вступают очередь сообщений и API C++. Допустим, вы запускаете фрагмент ниже:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
Вывод на консоль будет таким:
foo
baz
bar
Причина странного порядка в console.log
— в том, что Node выполняет setTimeout
как асинхронную операцию, даже если минимальное время ожидания setTimeout
равно нулю миллисекунд.
Node выгружает асинхронные задачи из стека вызовов в API C++ и выполняет их, задействуя системное ядро. Большинство системных ядер многопоточны и могут в фоновом режиме выполнять сразу несколько задач.
После завершения асинхронной задачи соответствующая функция обратного вызова помещается в очередь сообщений. Это очередь FIFO (First In — First Out, “первым на вход, первым на выход”), которая сохраняет правильную последовательность выполнения функций обратного вызова.
Цикл событий всегда проверяет стек вызовов и очередь сообщений, и если стек вызовов пуст, то он извлекает первую задачу из очереди сообщений и помещает в стек вызовов. Цикл событий ждет, пока стек вызовов опустеет — это объясняет, почему вывод предыдущего фрагмента кода регистрируется в странном порядке.
Очередь задач
Очередь задач была введена в Javascript ES6. Она похожа на очередь сообщений, но здесь асинхронным задачам не нужно ждать, пока все задачи в стеке вызовов будут выполнены. Это позволяет выполнить результат асинхронной задачи сразу же, как только завершится текущая задача из стека вызовов.
Функциональность промисов в JavaScript основана на очереди задач.
Заключение
Node с помощью цикла событий позволяет пользователям создавать однопоточные приложения с возможностью выполнения асинхронных операций и неблокирующего ввода-вывода. Благодаря однопоточности Node пользователям не нужно беспокоиться о потоковой передаче или параллелизме, что стало одной из причин огромной популярности Node.
Спасибо за чтение!
Читайте также:
- Создание простого веб-сервера с помощью Node.js и Express
- Найти и обезвредить: утечки памяти в Node.js
- Создание многопользовательской игры с использованием Socket.io при помощи NodeJS и React
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи: Janith Gamage, “Single-Threaded and Asynchronous — How Does Node Do It?”