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



Книга Однопоточность и асинхронность: как у 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.


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


723   0  

Comments

    Ничего не найдено.