杰瑞科技汇

Python greenlet如何实现协程切换?

greenlet 是一个轻量级的协程库,它让你可以手动控制代码的执行流,实现任务的切换,它是理解更高级的协程库(如 gevent)和 Python 原生 asyncio 的基础。

Python greenlet如何实现协程切换?-图1
(图片来源网络,侵删)

核心概念:什么是 Greenlet?

你可以把一个 greenlet 想象成一个极小的、可以随时暂停和恢复的“线程”,它不是操作系统级别的线程,而是由用户代码在 Python 解释器层面控制的。

关键特性:

  1. 手动切换:与 asyncio 不同,greenlet 的切换不是由事件循环自动驱动的,而是需要你显式地调用 greenlet.switch() 方法来切换执行权。
  2. 单线程:所有 greenlet 都在同一个操作系统线程内运行,因此它们的切换不涉及操作系统的上下文切换,开销非常小。
  3. 协作式多任务:一个 greenlet 只有在主动 switch 出去时,才会让出 CPU,其他 greenlet 才有机会运行,如果一个 greenlet 陷入了死循环,整个程序都会被阻塞。

安装 Greenlet

你需要安装它:

pip install greenlet

基本使用:创建和切换

让我们从一个最简单的例子开始,创建两个 greenlet 并在它们之间切换。

Python greenlet如何实现协程切换?-图2
(图片来源网络,侵删)
from greenlet import greenlet
def test1():
    print("Test1: 我是 test1,准备切换到 test2")
    # 切换到 g2,并可以传递数据
    gr2.switch("来自 test1 的问候")
    print("Test1: 我又回来了,来自 test2 的数据是:", gr2.dead) # gr2.dead 检查 g2 是否已结束
def test2():
    # 接收来自 g1.switch() 的数据
    msg = gr1.switch("来自 test2 的问候")
    print("Test2: 我是 test2,收到了消息:", msg)
    print("Test2: 我要结束了")
# 创建两个 greenlet 对象
# g1 会执行 test1 函数
# g2 会执行 test2 函数
gr1 = greenlet(test1)
gr2 = greenlet(test2)
# 启动 g1,test1 开始执行
# 第一次 switch 时,可以传递数据给 test1
print("主线程:启动 g1")
gr1.switch("主线程的初始数据")
print("主线程:g1 和 g2 都执行完毕")

输出结果:

主线程:启动 g1
Test1: 我是 test1,准备切换到 test2
Test2: 我是 test2,收到了消息: 来自 test1 的问候
Test2: 我要结束了
Test1: 我又回来了,来自 test2 的数据是: True
主线程:g1 和 g2 都执行完毕

代码解析:

  1. greenlet(test_func) 创建一个新的 greenlet 对象,它会在被 switch 到时执行 test_func 函数。
  2. gr.switch(value) 是核心操作:
    • 第一次调用:启动 greenlet gr,并开始执行其关联的函数。value 会被作为函数的返回值(而不是参数)。
    • 后续调用:暂停当前 greenlet 的执行,跳转到 gr 的暂停点(或起始点)继续执行。value 同样是作为 gr 函数的返回值。
    • gr 函数执行完毕或 grkill() 后,switch(gr) 会得到 None
  3. gr.dead 属性:greenlet 已经执行完毕,则返回 True,否则返回 False

Greenlet 的生命周期和父子关系

Greenlet 之间可以形成类似“调用栈”的父子关系。

  • 父 Greenlet:当一个 greenlet A 创建了另一个 greenlet BA B 的父 greenlet。
  • g.parent:可以访问或设置一个 greenlet 的父 greenlet。
  • g.switch() 和父 Greenlet:如果一个 greenlet 执行完毕,它会自动切换回它的父 greenlet。
  • g.throw():向 greenlet g 抛入一个异常,使其在 switch 到它时执行。

示例:父子关系和自动切换

Python greenlet如何实现协程切换?-图3
(图片来源网络,侵删)
from greenlet import greenlet
def child():
    print("子 Greenlet: 开始执行")
    print("子 Greenlet: 我要结束了,会自动切换回父 Greenlet")
    # 函数执行完毕,自动切换回 parent
def parent():
    print("父 Greenlet: 创建子 Greenlet")
    ch = greenlet(child)
    print("父 Greenlet: 切换到子 Greenlet")
    ch.switch() # 切换到 ch
    print("父 Greenlet: 从子 Greenlet 返回,继续执行")
# 主线程是 parent 的父 greenlet
p = greenlet(parent)
p.switch()

输出结果:

父 Greenlet: 创建子 Greenlet
父 Greenlet: 切换到子 Greenlet
子 Greenlet: 开始执行
子 Greenlet: 我要结束了,会自动切换回父 Greenlet
父 Greenlet: 从子 Greenlet 返回,继续执行

实际应用场景:模拟并发任务

greenlet 本身不提供 I/O 多路复用,所以不能直接用它来做异步网络编程,但它非常适合模拟那些需要“协作式”调度的任务,比如游戏逻辑、状态机等。

示例:简单的任务调度器

from greenlet import greenlet
import time
def task(name, count):
    for i in range(count):
        print(f"任务 {name} 正在执行,计数: {i}")
        # 模拟一个耗时操作,这里我们手动切换
        time.sleep(0.1) # 注意:time.sleep 会阻塞整个线程!
        # 在真实场景中,这里应该是一个可以被 greenlet 感知的 I/O 操作
        # 然后我们手动切换到下一个任务
        scheduler.switch()
def scheduler():
    tasks = [
        greenlet(task, ("任务A", 5)),
        greenlet(task, ("任务B", 5)),
        greenlet(task, ("任务C", 5))
    ]
    while tasks:
        # 轮询执行每个任务
        for t in tasks:
            if not t.dead:
                t.switch()
        # 移除已完成的任务
        tasks = [t for t in tasks if not t.dead]
        print("调度器:一轮任务结束,准备下一轮...")
        time.sleep(0.5)
print("主线程:启动调度器")
scheduler = greenlet(scheduler)
scheduler.switch()
print("主线程:所有任务完成")

输出结果:

主线程:启动调度器
任务 任务A 正在执行,计数: 0
任务 任务B 正在执行,计数: 0
任务 任务C 正在执行,计数: 0
调度器:一轮任务结束,准备下一轮...
任务 任务A 正在执行,计数: 1
任务 任务B 正在执行,计数: 1
任务 任务C 正在执行,计数: 1
调度器:一轮任务结束,准备下一轮...
...

注意:这个例子中的 time.sleep() 会阻塞整个线程,导致其他 greenlet 也无法运行,这只是为了演示切换逻辑,真正的 greenlet 应用场景是配合一个能处理阻塞 I/O 的库(如 gevent)。

Greenlet vs. asyncio vs. Threading

特性 greenlet asyncio threading
控制方式 手动 (switch) 自动 (事件循环) 抢占式 (操作系统)
并发模型 协作式多任务 协作式多任务 抢占式多任务
开销 极低 (纯Python切换) 低 (用户态切换) (内核态切换)
适用场景 理解协程、构建框架、状态机 高性能网络I/O、并发任务 CPU密集型任务、并行计算
GIL 受GIL限制 受GIL限制 受GIL限制(但可利用多核)
数据共享 需注意线程安全 需注意线程安全 需注意线程安全

重要注意事项

  1. 不是万能药greenlet 不能绕过 Python 的 GIL,对于 CPU 密集型任务,多线程或多进程仍然是更好的选择。
  2. 手动切换的复杂性:手动管理 switch 会让代码变得复杂,难以维护,这就是为什么现代 Python 更推荐使用 asyncio 这样的自动事件循环库。
  3. gevent 的强大geventgreenlet 的一个强大封装,它通过 monkey-patching(猴子补丁)技术,将 Python 标准库中的阻塞 I/O 操作(如 socket, threading 等)自动替换为非阻塞版本,这样,你可以在 gevent 的世界里,用同步的代码风格写出异步的 I/O 程序,而无需手动 switch

greenlet 是一个非常基础和底层的协程实现,学习它有助于你深入理解协程的工作原理,特别是任务切换的机制。

你应该什么时候用 greenlet

  • 学习目的:为了理解协程的底层实现。
  • 框架开发:如果你正在开发一个需要任务调度功能的框架,greenlet 提供了底层的构建块。
  • 特定场景:在那些逻辑上天然适合“协作式”调度的场景,如复杂的游戏状态机。

对于绝大多数应用开发,特别是网络编程,你更应该使用基于 greenletgevent 或者 Python 原生的 asyncio,它们提供了更高层次的抽象,让你能更轻松地编写高效的并发代码。

分享:
扫描分享到社交APP
上一篇
下一篇