Можно ли программно построить кадр стека Python и начать выполнение в произвольной точке кода?

Можно ли программно создать стек (один или несколько кадров стека) в CPython и начать выполнение с произвольной кодовой точки? Представьте себе следующий сценарий:

  1. У вас есть механизм рабочего процесса, в котором рабочие процессы могут быть написаны в сценарии на Python с некоторыми конструкциями (например, ветвлением, ожиданием/присоединением), которые являются вызовами механизма рабочего процесса.

  2. Блокирующий вызов, такой как ожидание или соединение, устанавливает условие прослушивателя в механизме диспетчеризации событий с каким-либо постоянным резервным хранилищем.

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

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

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

  6. Механизм диспетчеризации событий запускает событие, которое подхватывается условием ожидания.

  7. Механизм рабочего процесса считывает сериализованное состояние и стек и реконструирует поток со стеком. Затем он продолжает выполнение в точке, где была вызвана служба ожидания.

Вопрос

Можно ли это сделать с помощью немодифицированного интерпретатора Python? Более того, может ли кто-нибудь указать мне какую-нибудь документацию, которая может охватывать подобные вещи, или пример кода, который программно создает кадр стека и начинает выполнение где-то в середине блока кода?

Редактировать: Чтобы уточнить «немодифицированный интерпретатор Python», я не возражаю против использования C API (достаточно ли информации в PyThreadState для этого?), но я не хочу ковыряться в внутренности интерпретатора Python и необходимость создания модифицированного.

Обновление: в результате некоторого начального исследования можно получить контекст выполнения с помощью PyThreadState_Get(). Это возвращает состояние потока в PyThreadState (определено в pystate.h), которое имеет ссылку на кадр стека в frame. Фрейм стека хранится в структуре с типом PyFrameObject, который определен в frameobject.h. PyFrameObject имеет поле f_lasti (поддерживает bobince), у которого программный счетчик выражен как смещение от начала кодового блока.

Это последнее в некотором роде хорошая новость, потому что это означает, что пока вы сохраняете фактически скомпилированный блок кода, вы должны быть в состоянии реконструировать локальные переменные для стольких кадров стека, сколько необходимо, и перезапускать код. Я бы сказал, что это означает, что это теоретически возможно без необходимости создавать модифицированный интерпретатор Python, хотя это означает, что код все еще, вероятно, будет неудобным и тесно связанным с конкретными версиями интерпретатора.

Три оставшиеся проблемы:

  • Состояние транзакции и откат «saga», которые, вероятно, могут быть достигнуты путем взлома метакласса, который можно использовать для создания O/R-преобразователя. Однажды я создал прототип, поэтому у меня есть четкое представление о том, как это можно сделать.

  • Надежная сериализация состояния транзакций и произвольных локальных переменных. Этого можно добиться, прочитав __locals__ (доступный из кадра стека) и программно сконструировав вызов pickle. Тем не менее, я не знаю, какие здесь могут быть ошибки, если таковые имеются.

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

Обновление 2: PyCodeObject (code.h) содержит список сопоставлений адресов (f_lasti)-> номеров строк в PyCodeObject.co_lnotab (поправьте меня, если я ошибаюсь). Это может быть использовано для облегчения процесса миграции для обновления рабочих процессов до новой версии, поскольку указатели замороженных инструкций могут быть сопоставлены с соответствующим местом в новом сценарии, выполненным с точки зрения номеров строк. Все еще довольно грязно, но немного более многообещающе.

Обновление 3: я думаю, что ответом на этот вопрос может быть Stackless Python. Вы можете приостановить задачи и сериализовать их. Я не понял, будет ли это работать и со стеком.


person ConcernedOfTunbridgeWells    schedule 12.02.2009    source источник
comment
Отличный вопрос - я бы очень не хотел быть тем, кто должен отлаживать этот проект!   -  person Salim Fadhley    schedule 12.02.2009


Ответы (7)


Привязки expat python, включенные в обычный дистрибутив Python, программно создают кадры стека. Однако будьте осторожны, он использует недокументированные и частные API.

http://svn.python.org/view/python/trunk/Modules/pyexpat.c?rev=64048&view=auto

person Johan Dahlin    schedule 12.02.2009
comment
Спасибо. Очень помогает понять, как работает механизм. - person ConcernedOfTunbridgeWells; 12.02.2009

Обычно вам нужны продолжения, которые, как я вижу, уже являются тегом этого вопроса.

Если у вас есть возможность работать со всем кодом в системе, вы можете попробовать сделать это таким образом, а не иметь дело с внутренностями стека интерпретатора. Я не уверен, насколько легко это будет сохраняться.

http://www.ps.uni-sb.de/~duchier/python/continuations.html

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

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

person Dave Stenglein    schedule 12.02.2009
comment
Что мне нравится в «объектах действия» (подумайте о шаблоне «Команда»), так это то, что это также обеспечивает поддержку отката рабочего процесса. Хотя это компромисс между простотой и ясностью сценария рабочего процесса, с этой точки зрения это хороший подход. - person ConcernedOfTunbridgeWells; 12.02.2009

Python без стеков, вероятно, лучший… если вы не возражаете полностью перейти на другой дистрибутив Python. stackless может сериализовать все в python, а также свои тасклеты. Если вы хотите остаться в стандартном дистрибутиве Python, я бы использовал dill, который может сериализовать почти что угодно в Python.

>>> import dill
>>> 
>>> def foo(a):
...   def bar(x):
...     return a*x
...   return bar
... 
>>> class baz(object):
...   def __call__(self, a,x):
...     return foo(a)(x)
... 
>>> b = baz()
>>> b(3,2)
6
>>> c = baz.__call__
>>> c(b,3,2)
6
>>> g = dill.loads(dill.dumps(globals()))
>>> g
{'dill': <module 'dill' from '/Library/Frameworks/Python.framework/Versions/7.2/lib/python2.7/site-packages/dill-0.2a.dev-py2.7.egg/dill/__init__.pyc'>, 'c': <unbound method baz.__call__>, 'b': <__main__.baz object at 0x4d61970>, 'g': {...}, '__builtins__': <module '__builtin__' (built-in)>, 'baz': <class '__main__.baz'>, '_version': '2', '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x4d39d30>, '__doc__': None}

Dill регистрирует свои типы в реестре pickle, поэтому, если у вас есть какой-то код черного ящика, который использует pickle, и вы не можете его редактировать, то простой импорт dill может волшебным образом заставить его работать без обезьяньих исправлений стороннего кода.

Вот dill травит весь сеанс интерпретатора...

>>> # continuing from above
>>> dill.dump_session('foobar.pkl')
>>>
>>> ^D
dude@sakurai>$ python
Python 2.7.5 (default, Sep 30 2013, 20:15:49) 
[GCC 4.2.1 (Apple Inc. build 5566)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>> dill.load_session('foobar.pkl')
>>> c(b,3,2)
6

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

Вы также спросили, где он используется для сохранения состояния интерпретатора?

IPython может использовать dill для сохранения сеанса интерпретатора в файл. https://nbtest.herokuapp.com/github/ipython/ipython/blob/master/examples/parallel/Using%20Dill.ipynb

klepto использует dill для поддержки кэширования в памяти, на диске или в базе данных, что позволяет избежать повторных вычислений. https://github.com/uqfoundation/klepto/blob/master/tests/test_cache_info.py

mystic использует dill для сохранения контрольных точек для больших заданий по оптимизации, сохраняя состояние оптимизатора по мере его выполнения. https://github.com/uqfoundation/mystic/blob/master/tests/test_solver_state.py

Есть пара других пакетов, которые используют dill для сохранения состояния объектов или сеансов.

person Mike McKerns    schedule 25.01.2014

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

«Возобновляемые исключения» — действительно интересная языковая идея, хотя сложно придумать разумный способ, которым они могли бы взаимодействовать с существующими в Python блоками «try/finally» и «with».

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

person bobince    schedule 12.02.2009

Со стандартным CPython это осложняется смесью данных C и Python в стеке. Перестроение стека вызовов потребует одновременного восстановления стека C. Это действительно ставит его в слишком тяжелую корзину, поскольку потенциально может тесно связать реализацию с конкретными версиями CPython.

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

person ConcernedOfTunbridgeWells    schedule 12.09.2011

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

stackless утверждает, что может собирать тасклеты, если нет связанного «загроможденного» стека C (перегруженный - мой выбор фразы).

Я, вероятно, буду использовать eventlet и придумаю какой-нибудь способ травления «состояния», хотя я действительно не хочу писать явный конечный автомат.

person Brad Clements    schedule 06.10.2009
comment
К сожалению, OP на данный момент отложил эту проблему, поскольку исходный проект так и не был реализован: ^ p - person ConcernedOfTunbridgeWells; 06.10.2009

Как насчет использования joblib?

Я не совсем уверен, что это то, что вы хотите, но, похоже, это соответствует идее рабочего процесса, в котором этапы могут быть сохранены. Вариант использования Joblib, по-видимому, заключается в том, чтобы избежать пересчета, я не уверен, что вы пытаетесь сделать это здесь или что-то более сложное?

person lost    schedule 01.07.2013