【问题标题】:How do greenlets work?绿叶如何工作?
【发布时间】:2019-02-12 00:28:07
【问题描述】:

greenlets 是如何实现的? Python 使用 C 堆栈作为解释器并堆分配 Python 堆栈帧,但除此之外,它如何分配/交换堆栈,它如何挂钩到解释器和函数调用机制,以及它如何与 C 扩展交互? (有什么怪癖)?

源码中greenlet.c 的顶部有一些cmets,但它们有点不透明。 FWIW 我是从不熟悉 CPython 内部但非常熟悉低级系统编程、C、线程、事件、协程/协作线程、内核编程等的人的角度来的。

(一些数据点:don't use ucontext.hdo 2x memcpy, alloc, and free on every context switch。)

【问题讨论】:

    标签: python


    【解决方案1】:

    当一个 python 程序运行时,你实际上有两段代码在后台运行。

    首先,CPython 解释器 C 代码运行并使用标准 C 堆栈来保存其内部堆栈帧。其次,实际的 python 解释的字节码不使用 C 堆栈,而是使用堆来保存其堆栈帧。 greenlet 只是标准的 python 代码,因此行为相同。

    现在,在一个典型的微线程应用程序中,您将有数千个甚至数百万个微线程(greenlets)在各处切换。每个开关本质上等同于具有延迟返回(可以这么说)的函数调用,因此将使用一些堆栈。问题是,解释器的 C 堆栈迟早会遇到堆栈溢出。这正是 greenlet 扩展的目的,它旨在将堆栈的各个部分在堆中来回移动以避免此问题。

    如您所知,greenlets 包含三个基本事件、生成、切换和返回,因此让我们依次看一下:

    A) 繁殖体

    新生成的 greenlet 与它自己在堆栈中的基地址相关联(我们当前所在的位置)。除此之外,没有什么特别的事情发生。新生成的greenlet的python代码以正常方式使用堆,解释器继续使用C栈。

    B) 开关

    当一个greenlet从一个switchng greenlet切换到一个greenlet时,C栈的相关部分(从switchng greenlet的基地址开始)被复制到堆中。复制的 C-stack 区域被释放,并且切换的 greenlet 的解释器先前保存的堆栈数据从堆复制到新释放的 C-stack 区域。切换后的 greenlet 的 python 代码继续以正常方式使用堆。当然,扩展代码会跟踪所有这些(哪个堆部分转到哪个 greenlet 等等)。

    C) 回归

    堆栈未被触及,返回的greenlet的堆区域被python垃圾收集器释放。

    基本上就是这样,更多详细信息和解释可以在 (http://www.stackless.com/pipermail/stackless-dev/2004-March/000022.html) 找到,或者只需阅读 Alex 回答中指出的代码即可。

    【讨论】:

      【解决方案2】:

      如果获取并研究 greenlet 的 sources,您将在 greenlet.c 的顶部看到一条长长的评论,从第 16 行开始,摘要如下...:

      PyGreenlet 是一系列 C 堆栈 必须保存的地址和 以这样的方式恢复完整 堆栈范围包含有效数据 当我们切换到它时。

      并继续到第 82 行,准确总结您的问题。您是否研究过这些行(以及以下 1000 多个实现它们的行;-)...?我看不出有什么方法可以进一步压缩这 66 行,同时仍然有意义,在此处复制和粘贴它们也没有任何附加价值。

      基本上,您会看到没有真正的“挂钩”可言(可以这么说,C 级堆栈在“解释器的鼻子下”来回切换)除了与线程状态的微妙交互外-线程代码,以及从/到堆栈中保存和恢复greenlet的状态是基于memcpy调用加上对Python内存管理器的一些调用来分配/重新分配和释放来自或返回堆栈的空间.第 227-295 行中的三个函数处理繁重的工作,正如那里的评论所说,它们被包装在 298-310 处的几个 C 宏中“以简化维护”。

      其他 C 扩展可以通过其与 greenlet 扩展交互的接口在第 956-1045 行实现,并通过“CObject API”(当然是通过greenlet.h)公开here

      【讨论】:

      • 那个评论区让我很困惑,并没有真正回答我的问题。我只是希望得到一个简明的、高层次的总结/答案。无论如何感谢您的指点 - 希望它们对其他人有用(或者当我有更多时间进行源代码潜水时对我自己有用)。
      • @Yang,这 86 行 一个简洁的高级摘要 -- 达到了共同构成 .c 的 1410 行代码的大部分亮点和.h 文件! “堆栈切片由 memcpy 保存到由 Python 内存管理器分配和重新分配的内存中,并由 memcpy 恢复到堆栈中(然后释放 Python 内存)”更加简洁和更高级别(但我已经说了所有这在我的回答中!)但显然缺少一些重要的细节(因为它是两行文本,而不是 86 行;-)。你希望什么神奇的文字介于两者之间并让你开心?!
      • 对于初学者:什么是“greenlet 堆栈数据”?那只是为greenlet记账吗?或者它实际上是否包含某些 C 堆栈帧?什么是greenlet的“堆栈中的正确位置”?为什么总是有两个greenlet块/为什么堆上旧的? “greenlet 堆栈数据”下方的“与此 greenlet 无关的数据”是什么?区分“无关数据”和“较新数据”?等等。这是少量的C,但我也很忙,这与我目前的工作完全无关——只是出于好奇。这个问题突然出现在我的脑海中。再次,我很乐意稍后找到时间进行源潜水。
      • @Yang,greenlet 的堆栈数据是由于在 greenlet 中执行代码而在堆栈上的所有数据,因此它肯定包括堆栈帧(不确定您认为区分“C”堆栈来自另一种语言的框架?C、Assembly、Fortran 等等,它们是堆栈框架)。正确的位置正是堆栈数据最初所在的位置(因为始终涉及指向它的指针,因此无法在其他地方有用地重新加载)。始终在堆栈上的小 greenlet 块保留了该位置。这个明显的信息耗尽了评论中的可用空间,所以,'再见。
      • 为什么只保存和恢复堆栈就足够了,因为堆栈只是进程状态的一部分?堆上的所有对象会发生什么?
      猜你喜欢
      • 2018-09-15
      • 1970-01-01
      • 1970-01-01
      • 2015-01-30
      • 1970-01-01
      • 2018-01-20
      • 2016-06-03
      相关资源
      最近更新 更多