22

是否可以在 CPython 中以编程方式构造一个堆栈(一个或多个堆栈帧)并在任意代码点开始执行?想象以下场景:

  1. 您有一个工作流引擎,其中的工作流可以用 Python 编写脚本,其中包含一些调用工作流引擎的结构(例如分支、等待/加入)。

  2. 阻塞调用(例如等待或加入)在具有某种持久后备存储的事件调度引擎中设置侦听器条件。

  3. 您有一个工作流脚本,它调用引擎中的等待条件,等待稍后将发出信号的某些条件。这会在事件分派引擎中设置侦听器。

  4. 工作流脚本的状态,包括程序计数器(或等效状态)在内的相关堆栈帧被保留 - 因为等待条件可能会在几天或几个月后发生。

  5. 在此期间,工作流引擎可能会停止并重新启动,这意味着必须能够以编程方式存储和重建工作流脚本的上下文。

  6. 事件调度引擎触发等待条件拾取的事件。

  7. 工作流引擎读取序列化状态和堆栈,并使用堆栈重建线程。然后它在调用等待服务的地方继续执行。

问题

这可以用未经修改的 Python 解释器来完成吗?更好的是,谁能指出一些可能涵盖此类事情的文档或以编程方式构造堆栈帧并在代码块中间某处开始执行的代码示例?

编辑:为了澄清“未修改的 python 解释器”,我不介意使用 C API(PyThreadState 中是否有足够的信息来做到这一点?)但我不想去探索 Python 解释器的内部结构并拥有建立一个修改过的。

更新:通过一些初步调查,可以使用PyThreadState_Get(). 这将返回 a 中的线程状态PyThreadState(在 中定义pystate.h),该状态具有对 中的堆栈帧的引用frame。堆栈帧保存在 struct typedef'd to 中PyFrameObject,该结构定义在frameobject.h. PyFrameObject有一个字段f_lasti(对bobince的道具),它有一个程序计数器,表示为距代码块开头的偏移量。

最后一点是个好消息,因为这意味着只要您保留实际编译的代码块,您就应该能够根据需要为尽可能多的堆栈帧重建局部变量并重新启动代码。我想说这意味着理论上可以不必修改python解释器,尽管这意味着代码仍然可能会与特定版本的解释器紧密耦合。

剩下的三个问题是:

  • 事务状态和“传奇”回滚,这可能可以通过一种用于构建 O/R 映射器的元类黑客来完成。我确实构建了一次原型,所以我对如何实现这一点有一个很好的了解。

  • 稳健地序列化事务状态和任意局部变量。这可以通过读取__locals__(可从堆栈帧中获得)并以编程方式构造对 pickle 的调用来完成。但是,我不知道这里可能存在什么问题(如果有的话)。

  • 工作流的版本控制和升级。这有点棘手,因为系统没有为工作流节点提供任何符号锚。我们只有锚点 为了做到这一点,我们必须识别所有入口点的偏移量并将它们映射到新版本。手动操作可能可行,但我怀疑很难自动化。如果您想支持此功能,这可能是最大的障碍。

更新 2: PyCodeObject ( code.h) 中有一个 addr ( f_lasti)-> 行号映射列表PyCodeObject.co_lnotab(如果此处错误,请纠正我)。这可能用于促进将工作流更新到新版本的迁移过程,因为冻结的指令指针可以映射到新脚本中的适当位置,根据行号完成。仍然很混乱,但更有希望。

更新 3:我认为这个问题的答案可能是Stackless Python。 您可以暂停任务并将它们序列化。我还没有弄清楚这是否也适用于堆栈。

4

7 回答 7

10

普通 Python 发行版中包含的 expat python 绑定以编程方式构建堆栈帧。但请注意,它依赖于未记录和私有的 API。

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

于 2009-02-12T14:13:19.957 回答
6

您通常想要的是延续,我看到这已经是这个问题的一个标签。

如果您有能力使用系统中的所有代码,您可能想尝试这样做,而不是处理解释器堆栈内部。我不确定这将是多么容易持久。

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

在实践中,我会构建您的工作流引擎,以便您的脚本将操作对象提交给经理。经理可以在任何时候腌制一组动作,并允许它们被加载并再次开始执行(通过恢复提交动作)。

换句话说:创建自己的应用程序级堆栈。

于 2009-02-12T16:59:17.187 回答
3

Stackless python 可能是最好的……如果你不介意完全转向不同的 python 发行版。stackless可以序列化python 中的所有内容,以及它们的 tasklet。如果你想留在标准的 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 就可以神奇地使其工作而无需猴子修补第 3 方代码。

这是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保存对象或会话的状态。

于 2014-01-25T13:04:32.403 回答
2

您可以通过抛出异常并在回溯中向后退一帧来获取现有的堆栈帧。问题是没有办法在代码块的中间(frame.f_lasti)恢复执行。

“可恢复异常”是一个非常有趣的语言理念,尽管很难想出一种合理的方式来与 Python 现有的“try/finally”和“with”块进行交互。

目前,执行此操作的正常方法是简单地使用线程在与其控制器的单独上下文中运行您的工作流。(如果你不介意编译它们,或者协程/greenlets)。

于 2009-02-12T14:37:18.730 回答
2

使用标准 CPython,堆栈中 C 和 Python 数据的混合会使这变得复杂。重建调用堆栈需要同时重建 C 堆栈。这确实把它放在了太难的篮子里,因为它可能将实现与特定版本的 CPython 紧密耦合。

Stackless Python 允许对 tasklet 进行腌制,这提供了开箱即用的大部分功能。

于 2011-09-12T14:31:32.223 回答
1

我有同样类型的问题要解决。我想知道原始海报决定做什么。

stackless 声称只要没有关联的“受阻”C 堆栈(受阻是我选择的措辞),它就可以腌制小任务。

我可能会使用 eventlet 并找出某种腌制“状态”的方法,但我真的不想写一个明确的状态机..

于 2009-10-06T02:48:16.313 回答
1

使用joblib怎么样?

我不太确定这是您想要的,但它似乎符合拥有可以保留哪些阶段的工作流的想法。Joblib 的用例似乎是为了避免重新计算,我不确定这是您在这里尝试做的还是更复杂的事情?

于 2013-07-01T12:58:26.830 回答