2.2 Подходы Итак, главной задачей становится создание сервера, приложения для которого можно было описать, в виде кода, представленного в Листинге 1. В этом фрагменте кода функция send_and_wait(output) указывает серверу, что он должен вернуть содержание «output» пользователю и дождаться от него введение значения «a». Функция send(output) соответственно должна просто вернуть содержание «output» пользователю.
Листинг 1. Целевой вид обработчика многошаговой
бизнес-функции, основанный на продолжениях
Выбранный язык программирования для реализации программной среды – Python – не поддерживает продолжения, как таковые. Поэтому первой задачей становится эмуляция поведения продолжений доступными языковыми средствами.
2.2.1 Эмуляция CPS, основанная на замыканиях (англ. closure) Замыкание в программировании – процедура, которая ссылается на свободные переменные в своём лексическом контексте. Замыкание, так же как и экземпляр объекта, есть способ представления функциональности и данных, связанных и упакованных вместе.
Замыкание – это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. В записи это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции[6].
Замыкание связывает код функции с её лексическим окружением (местом, в котором она определена в коде). Лексические переменные замыкания отличаются от глобальных переменных тем, что они не занимают глобальное пространство имён. От переменных в объектах они отличаются тем, что привязаны к функциям, а не объектам.
Таким образом, существует возможность заключить код, содержащий оставшуюся часть бизнес-логики в замыкание и вернуть его в качестве возвращаемого значения наряду с прочими из исполняемой функции. В таком случае в задачу систему входят обработка возвращенного значения, сохранение замыкания в «дереве продолжений» и возврат клиенту страницы, содержащей идентификатор продолжения и прочие данные, возвращенные функцией.
При запросе к серверу, система должна по входному идентификатору взять замыкание (по сути, функцию) и выполнить её, передав в нее данные, полученные от пользователя.
Таким образом, исполняемый код подобной функции с замыканиями должен выглядеть, как в Листинге 2.
Листинг 2. Вид обработчика многошаговой бизнес-функции
с использованием замыканий
Приведенный в Листинге 2 код является рабочим, но он несколько не сходится с кодом, описанным в Листинге 1, к которому мы стремимся. Этот факт определяет главную и самую трудноразрешимую проблему такого подхода. Для того чтобы добиться главной цели, необходимо модифицировать исходный код перед выполнением. Учитывая тот факт, что блоки в Python определяются отступами, существует возможность вместо функции send_and_wait(), указанной в Листинге 1, подставлять заголовок замыкания, сдвигать последующие команды (с таким же или большим отступом) на установленный отступ, после чего дописывать оператор возврата значения, как в Листинге 2.
Такой подход таит в себе еще одну важную проблему. Взяв во внимание тот факт, что дальнейшая логика программно оказывается вложена в текущую, встает вопрос о реализации условных ветвлений, т.к. условие «вложенности» определяет для нас единственную возможность исключительно последовательного выполнения команд.
Третья проблема заключается в том, что замыкание хранит в себе состояние только локальных переменных функции, в которой она определена. Ввиду такого ограничения, не имеется возможности эмулировать полноценные продолжения как для вызываемых объектов (англ. callable objects), так и для функций, изменяющих глобальные переменные (что, в принципе, считается не лучшим тоном программирования).
2.2.2 Эмуляция работы продолжений, основанная на микропотоках (англ. microthreads) В основу второго способа решения поставленной задачи положены возможности альтернативного интерпретатора языка Python – Stackless Python. Следует отметить сразу, что Stackless Python не распространяется в качестве стандартного решения и его необходимо «собирать» собственноручно. Справедливости ради стоит отметить, что его «сборка» происходит традиционным способом с помощью утилиты «make» без осложнений.
Stackless Python – это версия языка Python, в которой вместо общего стека приложения используются фреймы, прикрепленные к каждому микропотоку, которая позволяет разработчикам использовать возможности разработки основанной на потоках без тех сложностей, которые традиционно связаны с обычными потоками.
Stackless Python предоставляет к использованию микропотоки, позволяющие оборачивать функции таким образом, чтобы их можно было запустить в микропотоке. Кроме того, Stackless Python позволяет использовать каналы (англ. channel) для двусторонней коммуникации между микропотоками, «планирование» (англ. scheduling) последовательности выполнения потоков и переключения между ними, а так же сериализацию (англ. serialization) микропотоков в строку или файловую систему с целью последующего восстановления и возобновления их работы.
Таким образом, работа такой системы выглядит следующим образом. Система состоит из обработчика запросов и, собственно, компонента управления системы. На старте системы инициируются обе эти компоненты и создаются два микропотока для них обеих. Кроме того, для обоих микропотоков определяется канал, по которому они будут общаться. Также при инициализации системы создается глобальный канал для общения между микропотоком компонента управления и микропотоком, который будет создан для каждой пользовательской функции.
Обработчик запросов ждёт входящего запроса, при поступлении которого определяет, какая запрошена функция приложения или какой идентификатор продолжения получен, а также другие пользовательские данные. Разобрав запрос, обработчик посылает в канал полученную информацию, которую ждет компонент управления системы.
Последний в свою очередь по типу переданного запроса определяет, что делать дальше. Предположим, пользователь запросил начальную точку входа в функцию. В таком случае, компонент управления создает микропоток для указанной функции (в этом случае он добавляется в конец планировщика, т.е. следующим после микропоток компонента управления) и переключается с помощью «планировщика» Stackless Python на него.
Далее выполняется код, описанный в Листинге 1 до вызова функции send_and_wait(). В вызове этой функции ссылка на текущий микропоток (микропоток вызванной функции) сохраняется в переменной и посылается глобальный канал вместе с выходными данными.
Это сообщение из канала получает компонент управления (в этот момент происходит переключение между микропотоком компонента управления и микропотоком вызванной функции), сохраняет текущее состояние микропотока вызванной функции с помощью сериализации, создавая при этом уникальный идентификатор, и прекращает выполнение сохраненного микропотока. Далее идентификатор продолжения и выходные данные посылаются в канал между компонентом управления и обработчиком.
Обработчик в свою очередь возвращает данные пользователю и ждет новый запрос, который вышеописанным образом передаст в компонент управления.
Предположим, следующий запрос придет с данными и идентификатором продолжения. В таком случае компонент управления системы вместо того, чтобы создавать новый микропоток, как это было в первом случае, просто извлечет по идентификатору сериализованный микропоток (по сути, продолжение), вставит его в цепь потоков и с помощью глобального канала передаст ему пользовательские данные и управление.
Это сообщение из канала получит функция send_and_wait(), которая в свою очередь вернет полученные данные в пользовательскую функцию, выполнение которой продолжится.
Таким образом, удается добиться такого же вида кода пользовательской функции, как и на Листинге 1, но данный подход порождает немало других проблем.
Во-первых, Stackless Python – это стороннее решение, которое не является общепринятым стандартом, вследствие чего не является распространенным решением, как было упомянуто выше.
Во-вторых, Stackless Python основан на том, что вместо традиционного стека (англ. stack) интерпретатора использует фреймы (англ. frame), которые относятся к каждому микропотоку. В Stackless Python существуют два вида фреймов – тривиальные и C-фреймы. Фреймы первого типа привязываются к микропотокам, которые обернутые вокруг простых функций, а C-фреймы – к микропотокам, обернутым вокруг сложных структур, таких как вызываемые объекты. В текущей реализации Stackless Python не существует возможности для сериализации и восстановления C-фреймов, с чем связана невозможность использования ничего, кроме функций. Справедливости ради стоит заметить, что разработчики Stackless Python обещали, что этот недостаток будет устранен.
И третья, на данный момент основная проблема заключается в том, что описанный сценарий работы системы не позволяет запускать ее в несколько потоков и обслуживать несколько пользователей одновременно ввиду того, что обработчик блокируется компонентом управления, а компонент управления блокируется пользовательской функцией. Дальнейшее развитие описанного подхода к разработке рабочей системы будет зависеть от решения именно этой проблемы, которая, по всей видимости, если и решается, то решается в рамках предоставленных средств языка Python и возможностей Stackless Python.
|