AndreyMelnikov.MyBlog # мой блог: IT-марафон.

Все мои посты

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

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

В 1946 г Джон фон Нейман, Герман Голдстайн и Артур Беркс в своей совместной статье изложили принципы построения и функционирования ЭВМ. Впоследствии на основе этих принципов производились первые два поколения компьютеров. В более поздних поколениях происходили некоторые изменения, хотя принципы Неймана актуальны и сегодня.
По одному из указанных принципов фон Неймана программа состоит из набора команд, которые выполняются процессором друг за другом в определенной последовательности. Выполнение инструкций (или программного кода) применительно к императивной парадигме программирования происходит последовательно или сверху вниз по мере чтения кода. Имея в своем арсенале такие элементарные конструкции как переменные, ветвления и циклы можно запрограммировать (описать на языке программирования) практически любую задачу.

В ходе написания программы мы неизбежно сталкиваемся с ситуацией многократного повторного использования одного и того же кода. Эти повторяющиеся участки программного кода, согласно принципу DRY - Don’t repeat yourself (не повторяй себя), принято локализовать и выделять в отдельные подпрограммы (или функции).

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

Пространство памяти внутри процесса, если описать очень упрощенно, делится на несколько сегментов:

  • Сегмент данных (обрабатываемых значений).
  • Сегмент команд (кода).
  • Стек вызова.

Сегмент данных - это динамическая память (куча - heap), в которой содержатся данные (объекты) для вычисления в коде.

На схеме выше, я постарался отобразить ход выполнения программы, в которой используется вызов функций. Весь исполняемый код располагается в сегменте команд. При выполнении главной программы __main__ интерпретатор языка доходит до точки вызова функции foo(), после чего, в стек вызова заносится адрес возврата. Это та точка в основной программе, куда будет возвращено управление после выполнения функции foo() посредствам оператора return. Формально, если коснуться более низкого уровня – ассемблера, после выполнения любой функции происходит вызов инструкции RET, которая уже использует стек с адресами возврата для передачи управления обратно в точку первоначального вызова. Точно также происходит и в случае еще одной вложенной функции bar(). После ее вызова в стек добавляется адрес возврата в функцию foo(). И, как только выполнится bar(), ее return вернет ход программы в foo(), используя адрес возврата из стека. А затем, после выполнения foo(), вытащит из стека самый нижний элемент – адрес возврата в __main__.

И вот тут мы плавно подошли к теме нитей выполнения. Дело в том, что во время выполнения всех вложенных функций главная программа __main__ , как-бы, ожидает пока выполнится foo(). А foo(), в свою очередь, ожидает выполнения bar(). Такая модель вызовов функций называется синхронными вызовами (или классическими вызовами).

Существует еще одна модель выполнения кода – асинхронная. В этом случае вызванная функция выполняется параллельно основной программе. Но, в таком случае, сразу встает вопрос о том, в какой момент, и в какую точку необходимо вернуть значение вызванной функции foo() в параллельно выполняющуюся __main__. И что делать, если foo() выполнилась раньше или позже от ожидаемого момента в основной программе.
Но асинхронная модель потому и названа таковой, что обе нити периодически обмениваются состоянием своего выполнения. И одна из нитей имеет возможность подстроиться под ход выполнения другой, ожидая ее в нужных моментах.
На схеме ниже я постарался привести такой пример выполнения.

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

В статье использовались материалы из курса лекций преподавателя кафедры информатики МФТИ Тимофея Хирьянова.