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

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

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

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

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

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

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

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

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

Вопрос

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

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

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

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

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

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

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

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

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

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

7 ответов

Решение

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

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

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

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

Как правило, вы хотите продолжения, которые, как я вижу, уже являются меткой по этому вопросу.

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

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

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

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

Stackless Python, вероятно, лучший... если вы не против полностью перейти на другой дистрибутив Python. stackless может сериализировать все в Python, а также их тасклетов. Если вы хотите остаться в стандартном дистрибутиве Python, я бы использовал укроп, который может сериализовать практически все в 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}

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

Вот 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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам