greenlet implemenation

本文深入解析了greenlet库的工作原理及其实现细节,探讨了如何通过greenlet实现更高效的并发控制,对比了greenlet与传统线程的区别,并详细介绍了greenlet在Python中的应用。

greenlet是一个python coroutine库,可以提供比thread更加细粒度的并发控制。由于thread通常是一个内核对象,而coroutine通常只是工作在用户模式下,所以不存在线程切换时的开销。当使用coroutine和thread来实现并发时,将会得到更大的并发度。因为thread虽然比起process而言已经十分轻量,但是毕竟还是有开销的,当系统中有太多的thread在运行,也会拖累系统的运行速度,因为操作系统忙于执行context switch。

在python中,我们从thread中说获得并发度要比想象的低很多,因为thread其实不是完全并发执行的,这其中涉及到一个GIL(Global Interpreter Lock)的问题。关于GIL的问题,可以参考Understanding the Python GIL

现在来简单讲解一下greenlet的实现:

switch to a new greenlet
stack overview

  high  +----------------+
   ^    |    old data    |
   |    +----------------+
   |    |    g_switch    |
   |    |----------------|
   |    |  dummy marker  |
   |    |----------------| <------- stack_stop  _
   |    |   other data   |                       /
   |    +----------------+                       |
   |    |  g_initialstub |                       |
   |    +----------------+                       |
   |    |  g_switchstack |                       |
   |    +----------------+                       /__ greenlet stack
   |    |   slp_switch   |                       /
   |    |----------------|                       |
   |    |   stsizediff   |                       |
   |    |----------------|                       |
   |    |    stackref    |                       |
   |    +----------------+ <------- stack_start _/
   |    | slp_save_state |
   |    +----------------+
   |    |     g_save     |
  low   +----------------+

在调用slp_save_state保存了target greenlet stack后, slp_switch会返回1, 此时控制流返回到g_initialstub.

此时会去调用PyEval_CallObjectWithKeywords, 调用greenlet的run函数, 从该函数返回后,将会切换到parent greenlet. 通常情况下,如果我们在创建greenlet时不指定parent,那么默认就是当前运行的greenlet: ts_current (green_new).
切换时,调用g_switch,并且将run的返回值传递给switch作为返回值。

当我们启动一个新的greenlet时,新的greenlet的stack必然在已有的greenlet之下。

running in a greenlet B, which is started by greenlet A, and try to switch to greenlet A.
stack overview

   high  +---------------+
    ^    |    old data   |
    |    +---------------+
    |    |    g_switch   |
    |    |---------------|
    |    |     marker    |
    |    +---------------+
    |    | g_initialstub |
    |    +---------------+
    |    |    call run   |
    |    +---------------+
    |    |    ... ...    |
    |    +---------------+
    |    |   g_switch    |
    |    |---------------|
    |    |     marker    |
    |    +---------------+
    |    | g_initialstub |
    |    +---------------+
    |    |    call run   |
    |    +---------------+
    |    |    ... ...    |
    |    +---------------+
    |    |    g_switch   |
    |    +---------------+
    |    | g_switchstack |
    |    +---------------+
    |    |   slp_switch  |
    |    |---------------|
    |    |   stsizediff  |
    |    |---------------|
    |    |    stackref   |
    |    +---------------+
    |    | slp_save_state|
    |    =================
   low

在切换到一个已经运行的greenlet时,slp_save_state会save更多的东西。原来切换到一个新的greenlet,只需要save prev greenlet(通常是calling greenlet)的状态. 而现在,由于已经有多个calling greenlet运行,所以必须依次save这些greenlets.

当切换到一个greenlet A,A.stack_stop < ts_current.stack_start时,在restore A的stack后,会在当中产生一段空档。

比如A->B->C,C switch to A, 此时stack中只有A,之后A switch to C,此时C中save的stack_copy会与A的stack产生一个空挡,其中这个空档就是原来的B造成的。

 

return from a greenlet
在调用完slp_save_state后,由于切换到的是一个已经运行的greenlet,所以我们会接着调用

    stsizediff = ts_target->stack_start - (char*)stackref

这里计算stack的差,因为在跳转到target greenlet后,我们target的stack必须是正确的。
还记得stackref吗,这个值是我们在调用到slp_switch时,esp的值,也就是说,我们每次slp_save_state的栈,其实是save到slp_switch为止的。

接着

    mov     eax, stsizediff
    add     esp, eax
    add     ebp, eax

这里将esp和ebp恢复到target greenlet调用slp_switch的状态。

之后,再调用slp_restore_state

    memcpy(g->stack_start, g->stack_copy, g->stack_saved);
    PyMem_Free(g->stack_copy);
    g->stack_copy = NULL;
    g->stack_saved = 0;

这里的g是target greenlet, 这里将heap中的stack copy恢复到原来的位置上
restore后,heap中的stack被free
也就是说,只要是正在运行的greenlet(ts_current),必定没有stack_copy.

当从slp_switch返回后,我们设置了thread state,把frame指向目标的top_frame,这样就达到了恢复python中ip指针的目的。

这里很重要的一点是,slp_save_state 和 slp_restore_state在同一个函数里。因为我们save的stack的状态包括slp_switch的stack frame,当然restore之后,我们也就必须在这个函数中恢复执行才可以。

 

switch to self

greenlet.getcurrent可以获得当前的greenlet,这样我们不必显式传递greenlet。当switch to self时,其实是一个no-op。

 

调用g_switch之后,由于切换到自身,所以PyGreenlet_ACTIVE返回true, 我们进入g_switchstack, 随后进入到slp_switch,这里会接着调用slp_save_state。

    while (ts_current->stack_stop < target_stop)
    {
        /* ts_current is entierely within the area to free */
        if (g_save(ts_current, ts_current->stack_stop))
            return -1;  /* XXX */
        ts_current = ts_current->stack_prev;
    }
    if (ts_current != ts_target)
    {
        if (g_save(ts_current, target_stop))
            return -1;  /* XXX */
    }

由于ts_current = ts_target,所以不会进入while和if,于是state不会被save,这是正确的,因为当前正在运行的greenlet的stack_copy = NULL。

随后进入到slp_restore_state

    if (ts_current->stack_stop == g->stack_stop)
        g->stack_prev = ts_current->stack_prev;
    else
        g->stack_prev = ts_current;

由于ts_current = g = ts_target,所以这里还是个no-op

接着返回到g_switchstack

    tstate->frame = ts_target->top_frame;

这里恢复thread state的frame,还是一个no-op,最后返回到g_switch,函数返回switch参数,结束。

 

在使用 `greenlet` 时,首先需要确保已经正确安装了该库。`greenlet` 是一个第三方库,因此需要通过 `pip` 安装[^1]。 ### 安装 greenlet 可以通过以下命令安装 `greenlet`: ```bash pip install greenlet ``` ### 导入 greenlet 安装完成后,在 Python 脚本中可以直接导入 `greenlet` 模块: ```python import greenlet ``` 如果在导入时遇到 `ModuleNotFoundError` 或 `ImportError`,可能是由于以下原因: - 使用了错误的 Python 环境,确保 `pip` 安装的库与当前运行的 Python 解释器一致。 - 安装过程中未成功下载或编译 `greenlet`,可以尝试重新安装。 ### 示例代码 以下是一个简单的 `greenlet` 使用示例,展示如何创建和切换协程: ```python import greenlet def test(name): print(f'this is in the test: name={name}') # 创建 greenlet 实例 gr1 = greenlet.greenlet(run=test, parent=None) # 切换到 gr1 并传递参数 gr1.switch('test') ``` ### greenlet 的父子关系 每个 `greenlet` 实例都有一个父 `greenlet`,用于控制协程执行流的回退。如果未指定,父 `greenlet` 默认是创建它的 `greenlet`。主 `greenlet` 是所有用户创建的 `greenlet` 的根节点,通常对应主线程执行的代码[^2]。 ### 常见问题 1. **greenlet 是否支持 Python 3?** `greenlet` 支持 Python 3,但在某些旧版本中可能存在兼容性问题,建议使用最新版本。 2. **greenlet 与 asyncio 的关系?** `greenlet` 是一种轻量级的协程实现,而 `asyncio` 是 Python 标准库中基于 `async/await` 的异步 I/O 框架。两者都可以用于并发编程,但 `greenlet` 更偏向于手动控制协程切换,而 `asyncio` 提供了更高级的异步编程模型。 3. **如何调试 greenlet 切换?** 可以通过 `greenlet.getcurrent()` 获取当前运行的 `greenlet` 实例,并结合 `parent` 属性查看协程树的结构。 4. **greenlet 是否线程安全?** `greenlet` 本身不是线程安全的,每个线程应使用自己的 `greenlet` 实例。如果需要跨线程通信,建议使用队列或其他线程安全机制。 5. **greenlet 是否可以嵌套创建?** 是的,可以在一个 `greenlet` 中创建另一个 `greenlet`,并且可以通过 `parent` 属性指定其父协程。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值