Что такое call stack
Перейти к содержимому

Что такое call stack

  • автор:

Стек вызовов (Call Stack) — JS: Асинхронное программирование

Перед тем, как погружаться в асинхронное программирование, разберём один важный момент, связанный с исполнением кода. Посмотрите на пример:

const data = [16, 64, 4]; const data2 = data.map(Math.sqrt); // [4, 8, 2] const predicate = (v) => v > 2; const data3 = data2.filter(predicate); // [4, 8] 

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

Если посмотреть на вторую строку, то видно, что вызывается метод map() , который внутри себя вызывает функцию Math.sqrt() . В реальных приложениях глубина проваливания внутрь функций может быть просто огромной, на сотни уровней. Во время выполнения кода, такое проваливание создает стек вызовов (call stack). Почему именно стек? Потому что так происходит процесс исполнения кода. Каждый внутренний вызов добавляет текущую функцию внутрь стека — и так до самой глубокой функции. Затем, когда происходит возврат, начинается раскрутка стека — из него по очереди (в обратном порядке) извлекаются функции и продолжают своё выполнение с того места, где внутренняя функция вернула результат.

Предположим, что у нас есть цепочка функций one() вызывает two() вызывает three(), который вызывает four():

const four = () => console.log('END!'); const three = () => four(); const two = () => three(); const one = () => two(); one(); // Запускаем 

Тогда процесс исполнения кода будет выглядеть так:

 three => four => three => two one 

То есть сначала идет погружение до самого вложенного вызова, затем подъем до первой функции в стеке вызовов.

Мы, как разработчики, видим стек вызовов каждый день в выводе ошибок. Backtrace (обратная трассировка) — не что иное, как стек вызовов, записанный в обратном порядке. Для демонстрации допустим ошибку в третьей строке нашего кода:

const data = [16, 64, 4]; const data2 = data.map(Math.sqrt); // [4, 8, 2] const predicate = (v) => unknown > 2; const data3 = data2.filter(predicate); // ReferenceError 

Запуск (код расположен в файле index.js) и вывод:

= (v) => unknown > 2; ^ ReferenceError: unknown is not defined at predicate (index.js:3:32) at Array.filter () at Object. (index.js:4:21) 

Важно понимать, что стек вызовов растёт только тогда, когда вызовы идут в глубину. Это видно по выводу: в бектрейсе не фигурирует первая и вторая строчки, в нём описана последовательность, начиная от вызова фильтра и дальше.

Механизм исключений в js, как и в других языках, полностью опирается на наличие стека вызовов. Более того, он создан, чтобы было удобно «раскручивать» этот стек. Любое возникающее исключение поднимается вверх по стеку вызовов, до тех пор пока не наткнется на конструкцию try/catch либо до тех пор пока стек вызовов не закончится.

const data = [16, 64, 4]; const data2 = data.map(Math.sqrt); // [4, 8, 2] const predicate = (v) => unknown > 2; try  const data3 = data2.filter(predicate); // ReferenceError > catch (e)  console.log('Catch it'); console.log(e.stack); > 

И хотя объявление функции predicate() , содержащей ошибку, находится вне блока try/catch, он всё равно поймает ошибку внутри этой функции, так как predicate() вызывается внутри по цепочке.

(index.js:3:32) at Array.filter () 

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

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Call stack

Стек вызовов (call stack) — это механизм для интерпретаторов (таких как интерпретатор JavaScript в веб-браузере) для отслеживания текущего местонахождения интерпретатора в скрипте, который вызывает несколько функций, — какая из функций выполняется на данный момент, какие функции вызываются изнутри этой (выполняемой) функции, какая будет вызвана следующей и т. д.

  • Когда скрипт вызывает функцию, интерпретатор добавляет её в стек вызовов и потом начинает её обработку.
  • Любые функции, вызванные этой функцией, добавляются в стек вызовов и выполняются, как только происходит их вызов.
  • Когда выполнение основной функции завершено, интерпретатор снимает её со стека вызовов и возобновляет выполнение кода в списке основного кода с той точки, где остановился до этого.
  • Если стек занимает больше места, чем ему было присвоено, это приводит к ошибке переполнения стека («stack overflow» error).

Пример

function greeting()  // [1] Some code here sayHi(); // [2] Some code here > function sayHi()  return "Hi!"; > // Invoke the `greeting` function greeting(); // [3] Some code here 

Код выше будет выполнен следующим образом:

  1. Игнорирование всех функций, пока не будет достигнуто место вызова функции greeting() .
  2. Вызывается функция greeting().
  3. Функция «greeting» помещается в очередь стека вызовов.

Примечание: Очередь стека вызовов: — greeting
Примечание: Очередь стека вызовов: — greeting — sayHi
Примечание: Очередь стека вызовов: — greeting
Примечание: Очередь стека вызовов: ПУСТО

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

Found a content problem with this page?

  • Edit the page on GitHub.
  • Report the content issue.
  • View the source on GitHub.

This page was last modified on 3 авг. 2023 г. by MDN contributors.

Your blueprint for a better internet.

Call Stack, Event Loop, and Task Queue

В этой статье мы попытаемся понять как работает JavaScript под капотом и, в частности, что же такое CallStack.

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

Здесь мы можем видеть CallStack в действии. Сначала запускается программа, затем вызов функции bazinga записывается в стек, после этого bar вызывается внутри функции bazinga, она также записывается в стек. Аналогично функция foo вызывается внутри bar, и она также попадает в стек. Теперь, когда выполнение функции foo завершено, она выкидывается из стека первой, а затем bar и наконец bazinga. В самом конце main, то есть наша программа тоже выходит из стека. Так выполняется синхронный код в JavaScript.

Task Queue — это очередь из сообщений различных API предоставленных окружением будь то node.js или браузер. Эти сообщения нужны для того, чтобы навешать на них функции обратного вызова после того, как CallStack будет обработан.

Event Loop это цикл который следит за тем, чтобы все функции из CallStack были выполнены. А если это случилось, то может добавлять функции обратных вызовов из Task Queue.

В гифке сверху мы визуализировали то, как выполняются операции в JavaScript. Три бокса, и у каждого своя роль в асинхронных операциях. Левый — CallStack, который мы уже обсудили. Правый — Web API (например, setTimeOut, setInterval и события браузера). Нижний бокс— Task Queue. И синий кружочек — EventLoop.

В главном треде main запускает нашу программу, затем foo попадает в стек и тут же исчезает после выполнения. Теперь выполняется setTimeout, добавляя в очередь функцию обратного вызова, и тоже исчезает. Сразу после этого, bar попадает в стек без каких-либо блокировок и вылетает, как только его выполнение завершится. Теперь, движок дождется момента когда время указанное в setTimeout пройдет и обратный вызов функции будет помещен в очередь. А после того, как CallStack стал пустым EventLoop переложит туда задачу из Task Queue. Затем снова запускается основной поток, и обратный вызов выполняется и уходит из стека.

Что такое call stack

Стек вызова функций (Call stack in programming languages)

Как рассматривать стек вызова функции (Call Stack) в контексте языков программирования. Хочу сказать, что стек вызова не совсем то, что демонстрирует нам большинство материалов. Чтобы окончательно понять что такое стек вызова, необходимо вернуться к азам и рассмотреть как работает стек вызова в операционной системе, почему именно так? Потому что нельзя рассматривать стек вызова функций оторванно от физической памяти и возможностью управления ею и тем самым не надо забывать, что стек вызова функций это не «виртуальная» операция, а конкретное место в памяти выделяемое операционной системой для выполнение программного кода функции, которая была запущена внутренним или внешним API (будь-то браузера, IDE или движка какого либо языка).

Хочу определить что такое стека вызова функции в низкоуровневых (low-level programming languages) языках (Assembler, C++):

(В современных низкоуровневых языках есть современные библиотеки позволяющие поддерживать работу ООП и контролировать стек функции не на физическом уровне, а использованием вывода консоли: Poppy, Pantheios)

Шаг 1: запуск лейаута кода.

В лейауте кроме самих инструкций находится дополнительная информация (переменные, декларативные данные, дополнительные технические данные и …)

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

  1. Первый вызов какой либо программной инструкции происходит от модуля ОС, этот запуск так и получил название уровень модуля (это первая инструкция, которая начинает свою работу с модуля памяти ОС) (далее приведено стандартное название в языках main());

Инструкция получена, создается сам стек в котором основная часть — выделенное место для выполнения операций, а сегмент стека для хранения данных для возврата результата выполнения функции.

На определенном этапе, функция либо возвращает результат либо вызывает следующую функцию. В сегмент стека кладется информация не о времени, месте или каких-то других параметрах вызова функции, только адрес(в бинарной нотации), куда надо вернуть результат вызова функции. Так же в этот сегмент попадают локальные переменные.

Инструкция возврата вызова, это не место вызова этой функции, а следующая за ней инструкция.

Сегмент стека (который мы сейчас по простоте называем стеком вызова) это отдельный участок памяти, в котором храняться инструкции о возврате результатов вызванных функции. Используется метод: кто пришел последний — уйдет первый (LIFO).

В языках подобных С++ имплементированы два метода: Call и Ret, Call создает инструкцию с адресом(в бинарной нотации) и кладет на верхний уровень стека, Ret не содержит адреса, этот метод просто забирает верхнюю инструкцию и возвращает нас по указанному адресу. Сам стек, после возвращения данных и выполнения функций уничтожается и данные из него недоступны.

Далее необходимо понять, как высокоуровневые языки реализовывают работу стека вызова. Давайте рассмотрим два случая: языки использующие многопоточность и однопоточные языки. Почему именно такие варианты? Потому что именно многопоточность (multithreading) и однопоточность определяет взаимодействие виртуальной и физической памяти ресурса.

Языки использующие многопоточность рассмотрим на примере Java.

В движок java имплементирована специальная коллекция Stack, у которой есть два метода push(добавит данные) и pop(удалить данные). Виртуальная машина Javaведет запись всех вызванных функций и с каждым вызовом функции создает StackTraceElement. После завершения выполнения функции StackTraceElement уничтожается, поэтому информация о стеке вызовов всегда актуальная. С Помощью метода getMethodName можно всегда получить информацию о методе верхнего элемента StackTraceElement, а значит о методе вызова функции.

Javascript: Javascript однопоточный скриптовый язык использующий очередь функций обратного вызова(callback functions queue) с методом FIFO (первый пришел — первый уйдешь).

Два варианта взаимодействия физической и виртуальной памяти в Javascript:

  1. Работа JS в браузере (цикл событий — event loop с очередью исполняемых колбэков). За переполнением стека (опять же того сегмента с информацией о адресе возврата) тут следит как ОС так и API браузера и V8 двигатель JS. У API Google Chrome есть метод, который после 16 тыс записей стек-кадров (stack frames) выдает сообщение о переполнении стека. после этого браузер выводит сообщение об ошибке или невозможности получить данные для возврата функции и очистки стека. (Источник:https://medium.com/@gaurav.pandvia/understanding-javascript-function-executions-tasks-event-loop-call-stack-more-part-1-5683dea1f5ec; )

Сам стек как и базовые использует LIFO, а очередь колбэков — FIFO. Так же в V8 имплементированы методы push и pop, декларация идентична методам, реализованным в Java.

Методы Javascript использующие таймеры использующие API OS и таймеры которые работают на API браузера. Таймеры всегда передвигают выполнение функции-колбэка в конец стека вызовов. Что это значит? Это значит, что в браузере выполняя методы setTimeout, setInterva сначала запускается таймер, а лишь потом функция-колбэк попадает в очередь ожидания. Если в момент попадания колбэка стек переполнен, колбэку приходится ждать своей очереди. Таймеры запущенные вне браузера реализованы на основании либо внутренних методов V8 либо при обращении к API окружения (API OS).

Особенностью языка Javascript является технология ajax или иначе возможность отправки асинхронных запросов (XMLHTTPRequest) и выполнения каких либо операций без задержки выполнения основного кода. XMLHTTPRequest может быть отправлен синхронно, но при этом весь остальной код будет дожидаться либо результата отправки запроса, что порой нецелесообразно. Технология ajax реализована исключительно в браузерах, что делает выделяет Javascript из группы языков использующих async. Асинхронные запросы в JS используют цикл событий с очередью колбэков (event loop with callback queue). В остальных языках программирования тоже есть возможность запуска асинхронного кода (Python (asyncio), C#(async/await)), что позволяет выполнять какие-то операции без задержки выполнения основных функций, принципом тут тоже выступает цикл событий, где функция колбэк помещается в очередь ожидания. Принцип работы очереди — FIFO.

Очень много в сети информации, поэтому кратко.

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

2.Работа стека на сервере (server side).Рассмотрим на примере V8 в node.js

Цикл событий в Node.js реализован на базе языка C++.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *