Модель параллелизма в Javascript

Также найдите эту статью на Scaler Topics

Идея параллелизма в языке, который работает в один поток, очень интересна. Для тех из вас, кто еще не знает, параллелизм — это просто выполнение нескольких действий в одно и то же время. Точно так же, как вы делаете разные вещи на разных вкладках, пока читаете эту статью.
Представьте, что вы можете делать только одну вещь одновременно. Никакого Netflix, пока вы едите! Конечно, это может помочь вам сосредоточиться, но в повседневной жизни вы можете чувствовать себя менее продуктивным.
Ваши ресурсы используются недостаточно эффективно. Мало того, вы видите своих современников, которые способны делать несколько дел одновременно. Конспект этой статьи как раз об этом. Вместо вас мы будем говорить о javascript.

💡 Поскольку вы здесь читаете о цикле событий в JS, эта статья предполагает, что вы хорошо знаете фундаментальные принципы Javascript. Вы можете начать с «Ссылки», чтобы прочитать об основах.

Javascript начинался как однопоточный синхронный язык. Язык программирования, который должен работать в один поток, означает, что он сможет делать только одну вещь за один раз. Выполнение одного действия за один раз требует соблюдения некоторого порядка выполнения. Таким образом, он называется синхронным.

В случае синхронного Javascript код будет выполняться строка за строкой.
Вот пример:

let a = 5;
let b = 10;
let c = b/a;

print(c)
Войти в полноэкранный режим Выйти из полноэкранного режима

Каждая строка выполняется одна за другой. Синхронный код также называется блокирующим. Это происходит потому, что строка 2 не может быть выполнена до тех пор, пока не будет выполнена строка 1. Вы можете не заметить «блокировку» в этом примере, потому что операции обычно выполняются быстро.

let a = 5, b = 10;
let result = thisCalculationTakesTwoMinutes(a,b)
let c = 15, d = 30;
let result2 = d/c;
print(result2)
Вход в полноэкранный режим Выход из полноэкранного режима

В приведенном выше примере строка 2 включает вычисление, которое занимает две минуты. Пока происходит этот вызов, вы не можете перейти к строке 3, даже если вычислительные мощности вашей машины позволяют сделать больше в это время.

💡 Именно здесь концепция параллелизма становится важной в Javascript. Учитывая, что язык однопоточный, в отличие от других языков, таких как Java, которые могут использовать несколько потоков для достижения параллелизма, этот любопытный случай параллелизма в JS становится еще более интересным.

С появлением параллелизма javascript очень серьезно подошел к вопросу «ваш ребенок может больше». Прежде чем перейти к детальному обсуждению параллелизма, давайте попробуем определить некоторые фундаментальные понятия, которые тесно связаны между собой.

Однопоточность:
Javascript является однопоточным языком. Это означает, что все приложения, когда-либо написанные с использованием Javascript, будут использовать только один поток для выполнения. Эта идея однопоточности непопулярна среди таких языков, как Java и Python, которые используют многопоточность. (Выполнение нескольких задач одновременно несколькими потоками). Однопоточность также означает, что порядок выполнения всегда один за раз.

Неблокирование:

Проще говоря, неблокируемость означает, что выполнение программы не будет заблокировано из-за какого-либо кода, выполнение которого занимает необычно много времени. Если первая строка работает медленно, она не должна блокировать выполнение второй строки. Разве мы не хотели этого?

Асинхронный: Асинхронный означает, что две или более задачи могут выполняться параллельно. Это может показаться противоречащим тому, что вы прочитали выше, но это правда. В Javascript вы можете делать много вещей одновременно. В последующих статьях мы рассмотрим это более подробно.

Как выполняется код в Javascript?

Чтобы сохранить порядок того, что выполняется в данный момент, Javascript использует стек во время выполнения кода. Возможно, вы уже знаете, что выполнение происходит внутри контекстов выполнения. Если нет, то считайте, что контексты выполнения — это объекты javascript, которые содержат выполнение кода. Функции имеют локальные контексты выполнения, которые создаются при вызове функций, а все остальное выполняется на глобальном контексте выполнения.

Контекст выполнения кода, который выполняется в данный момент, будет находиться на вершине стека. По завершении выполнения контекст выполнения извлекается из стека. Эта структура данных известна как стек вызовов.

Давайте разберемся на примере:

let sayHello = (name) ⇒ { console.log(`Hello ${name}` }
let sayBye = (name) ⇒{ console.log(`Bye ${name}` } 
let communicate() = (thingToSay) = {console.log(`${thingsToSay`)}

console.log(”Communication Started”)
sayHello("Harsh")
communicate("This is how code is executed in JS")
sayBye("Harsh")
console.log(”Communication Over”)
Вход в полноэкранный режим Выход из полноэкранного режима

«Выполнение действий» в Javascript достигается с помощью стека вызовов и JS Engine. В то время как JS Engine выполняет код, стек вызовов поддерживает порядок выполнения. Для начала представьте стек вызовов как обычный стек (структуру данных), в котором хранятся некоторые объекты. На вершине стека находится объект, представляющий вызов в текущем выполнении, который по завершении/прерывании будет выскочен и управление будет передано следующему вызову/контексту выполнения.

Хотя это не относится к данному вопросу, но вы должны заметить, что sayHello, sayBye и communicate являются выражениями функций и, следовательно, инициализация для них будет отличаться от объявления функции.

Вот что должно произойти во время выполнения приведенного выше кода.

  1. Создается глобальный контекст выполнения и сразу же помещается в стек вызовов, который ранее был пуст. Как только контекст оказывается в стеке вызовов, JS-движок начинает выполнять код.
  2. Когда элемент управления достигает строки 4, он выводит на консоль сообщение «Communication Started».
  3. Дойдя до строки 4, элемент управления находит вызов функции, которая создает локальный контекст выполнения, и сразу же контекст выполнения sayHello помещается на вершину стека вызовов.
  4. JS Engine будет выполнять код только в контексте верхнего элемента стека, поэтому код внутри sayHello начинает выполняться и на консоль выводится «Hello Harsh».
  5. Поскольку нет кода для дальнейшего выполнения, контекст sayHello выводится из стека, и элемент управления переходит к следующей строке.
  6. Элемент управления находит вызов другой функции, и процесс с 3 по 5 происходит для функции communicate. В результате на консоль выводится «Вот как выполняется код в JS».
  7. Когда элемент управления достигает следующей строки, происходит аналогичный процесс и на консоль выводится «Bye Harsh».
  8. Теперь элемент управления возвращается к глобальному контексту выполнения, который был выполнен только наполовину. Элемент управления начинает выполнение с того места, на котором он остановился, и в консоль выводится сообщение «Communication Over».
  9. Поскольку в глобальном контексте выполнения не осталось кода, он всегда вытаскивается из стека вызовов, и стек вызовов пуст.

Но где же здесь параллелизм?

Параллелизм — это способность выполнять несколько действий одновременно. Он сопровождается асинхронностью, которая является душой современного Javascript. Он приносит с собой еще одну специальную структуру данных, называемую очередью обратного вызова. Давайте попробуем понять это на классическом примере setTimeout.

Да, setTimeout знакомит большинство разработчиков с душой (асинхронностью) Javascript. Есть ли у него какое-либо другое применение — это спор на другой день.

setTimeOut принимает два параметра, функцию обратного вызова и время в милисекундах.

Вернемся к примеру:

console.log("Start")
setTimeOut(function delayThis(){console.log(”This will be delayed”)}, 5000)
console.log("End")
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы думаете, что получится из приведенных выше строк кода?

  • При каждом вызове setTimeOut стек вызовов теперь содержит setTimeOut и ждет, пока произойдет задержка, прежде чем перейти к следующей строке
  • Когда вызывается setTimeout, вызов функции попадает на вершину стека вызовов, но тут же сбрасывается. Следующая строка выполняется немедленно (не дожидаясь задержки).

Давайте попробуем понять порядок выполнения. Когда код начинает выполняться, первое, что записывается в консоль, это «Start». Это довольно очевидно.

Когда управление доходит до второй строки, вызывается setTimeout и регистрируется функция обратного вызова для будущего выполнения.

💡 Если вы еще не знаете, функция обратного вызова — это функция, которая передается другой функции в качестве параметра. Поскольку функции являются гражданами первого класса в Javascript, они могут быть переданы или возвращены из другой функции. Подробнее об этом вы можете прочитать в этом «блоге».

Элемент управления не ждет, пока произойдет задержка. По сути, задержка не означает «задержку в программе», а скорее означает «задержку в выполнении функции обратного вызова». И то, и другое — совершенно разные вещи.

Далее управление немедленно переходит к третьей строке, и контекст выполнения, связанный с вызовом setTimeout, уничтожается. Когда это происходит, функция обратного вызова все еще находится в памяти, просто не в этом контексте выполнения. Во время регистрации функция обратного вызова перемещается в другую ячейку памяти. Когда обратный вызов готов к выполнению (прошло 10 секунд) для выполнения, он перемещается обратно в структуру данных, точно так же, как все вызовы функций перемещаются в стек вызовов для выполнения.

💡 Примечание: Не все обратные вызовы имеют одинаковый приоритет. Некоторые обратные вызовы используют специальную очередь, известную как очередь микрозадач/очередь заданий, которая имеет более высокий порядок приоритета, чем очередь обратных вызовов. Это вводит более интересные понятия, такие как голодание в очереди. Очередь заданий/очередь микрозадач выходит за рамки данной статьи, но вы можете прочитать больше об этом здесь [URL].

Вот более практичный пример:

console.log("Start")
asyncNetworkCall()
console.log("End")
Вход в полноэкранный режим Выход из полноэкранного режима

💡 Предположим, что asyncNetworkCall реализован как асинхронная функция. Это означает, что пока сетевой вызов находится в функции, элемент управления может перейти к строке 3 и записать что-нибудь в консоль.

Цикл событий

Прежде чем мы начнем говорить о цикле событий. Давайте снова вернемся к строительным блокам. В вашем любимом редакторе кода висит некоторый код javascript. К этому моменту мы уже знаем, что JS Engine и стек вызовов — это основные вещи, отвечающие за выполнение javascript-кода. Для того чтобы код был выполнен, он должен быть помещен в стек вызовов, чтобы JS Engine выполнил его.

Теперь вспомните функцию обратного вызова в приведенном выше примере. Последний раз мы слышали о ней, когда она находилась в новом причудливом блоке памяти под названием очередь обратного вызова. По истечении 10 секунд она должна быть выполнена. Цикл событий — это такой механизм, который обрабатывает это. Это означает, что он управляет жизненным циклом функции обратного вызова от очереди обратного вызова до стека вызовов.

💡 Когда бы вы ни читали о цикле событий, думайте о нем как о механизме, а не как о чем-то еще.

Но как он работает?

Давайте начнем с констатации универсальной истины — «Стек вызовов должен быть пустым, чтобы событийный цикл мог принести в него хоть что-то».

Всякий раз, когда стек вызовов пуст, первая поступившая единица в очереди обратных вызовов приносится в стек вызовов циклом событий, который наблюдал за стеком вызовов и очередью обратных вызовов.

💡 Вы заметили?
setTimeout в ‘n’ секунд не гарантирует выполнение точно по истечении ‘n’ секунд, скорее он гарантирует минимальную задержку в ‘n’.
Это происходит потому, что после определенного предела функция обратного вызова переходит в очередь обратных вызовов (объявляя о готовности к выполнению), где цикл событий должен выбрать ее для фактического выполнения.

Очень интересной информацией о цикле событий является то, что цикл событий не блокируется. Операции, которые потенциально могут потребовать ожидания, выполняются с помощью обратных вызовов и событий.

💡 Визуализация событийного цикла.

Резюме:

  • JS является однопоточным. Он имеет только один стек вызовов.
  • Выполнение кода в javascript всегда происходит построчно.
  • Код на javascript выполняется движком JS Engine, который использует стек вызовов для определения порядка выполнения.
  • Цикл событий — это механизм, с помощью которого «вызовы, ожидающие выполнения» в очереди обратных вызовов/очереди заданий, могут быть помещены в стек вызовов.
  • Чтобы любое событие из очереди обратных вызовов/очереди заданий попало в стек вызовов, стек вызовов должен быть пустым.

Оцените статью
devanswers.ru
Добавить комментарий