Однопоточность и асинхронность: как у Node это получается?

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.

Спасибо за чтение!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи: Janith Gamage, “Single-Threaded and Asynchronous — How Does Node Do It?”

Предыдущая статьяКак находиться в потоке, программируя в парах
Следующая статья3 инструмента для отслеживания и визуализации выполнения кода на Python