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

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

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

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

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

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

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

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

问题

这可以使用未经修改的 Python 解释器来完成吗?更好的是,任何人都可以向我指出一些可能涵盖此类内容的文档,或者以编程方式构造堆栈帧并在代码块中间的某个位置开始执行的代码示例吗?

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

更新: 从一些初步调查中,人们可以通过以下方式获取执行上下文 PyThreadState_Get(). 。这将返回线程状态 PyThreadState (定义于 pystate.h),它引用了堆栈帧 frame. 。堆栈帧保存在类型定义的结构中 PyFrameObject, ,其定义在 frameobject.h. PyFrameObject 有一个字段 f_lasti (道具 博宾斯),它有一个程序计数器,表示为距代码块开头的偏移量。

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

剩下的三个问题是:

  • 事务状态和“saga”回滚,这可能可以通过用于构建 O/R 映射器的元类黑客技术来完成。我确实构建过一次原型,所以我很清楚如何实现这一点。

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

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

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

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

有帮助吗?

解决方案 5

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

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

其他提示

普通 Python 发行版中包含的 expat 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中,加上它们的tasklet。如果你想留在标准 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}

Dill 将其类型注册到 pickle 注册表,所以如果您有一些使用的黑盒代码 pickle 并且您无法真正编辑它,那么只需导入 dill 就可以神奇地使其工作,而无需对第 3 方代码进行猴子修补。

这是 dill pickling 整个解释器会话...

>>> # 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 也有 一些好的工具 帮助您了解当代码失败时导致酸洗失败的原因。

你还问它是用来保存解释器状态的?

Python 可以使用 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”块进行交互的合理方式。

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

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

stackless 声称只要没有关联的“阻碍”C 堆栈(阻碍是我选择的措辞),它就可以pickle tasklet。

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

怎么样使用 作业库?

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top