Python greenlet 协程

本文详细介绍了Python中greenlet库的使用方法,包括协程的基本概念、greenlet库的使用方式、协程间的异常处理及数据传递等核心内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

协程(Coroutine),它也被称为微线程。回顾一下,协程可以在一个函数执行过程中将其挂起,去执行另一个函数,并在必要时将之前的函数唤醒。在Python的语言环境里,协程是相当常用的实现“并发”的方法。上一篇的例子中,我们演示了如何使用yield关键字来实现协程,不过这个看上去非常不直观。这里我们要介绍一个非常好用的框架greenlet,很多知名的网络并发框架如eventlet,gevent都是基于它实现的。

第一个例子

沿袭我们一直以来的习惯,先从例子开始,这次偷个懒,直接把官方文档中的例子拿过来:

这里创建了两个greenlet协程对象,gr1和gr2,分别对应于函数test1()和test2()。使用greenlet对象的switch()方法,即可以切换协程。上例中,我们先调用”gr1.switch()”,函数test1()被执行,然后打印出”12″;接着由于”gr2.switch()”被调用,协程切换到函数test2(),打印出”56″;之后”gr1.switch()”又被调用,所以又切换到函数test1()。但注意,由于之前test1()已经执行到第5行,也就是”gr2.switch()”,所以切换回来后会继续往下执行,也就是打印”34″;现在函数test1()退出,同时程序退出。由于再没有”gr2.switch()”来切换至函数test2(),所以程序第11行”print 78″不会被执行。

所以,程序运行下来的输出就是:

很好理解吧。使用switch()方法切换协程,也比”yield”, “next/send”组合要直观的多。上例中,我们也可以看出,greenlet协程的运行,其本质是串行的,所以它不是真正意义上的并发,因此也无法发挥CPU多核的优势,不过,这个可以通过协程+进程组合的方式来解决,本文就不展开了。另外要注意的是,在没有进行显式切换时,部分代码是无法被执行到的,比如上例中的”print 78″。

父子关系

创建协程对象的方法其实有两个参数”greenlet(run=None, parent=None)”。参数”run”就是其要调用的方法,比如上例中的函数test1()和test2();参数”parent”定义了该协程对象的父协程,也就是说,greenlet协程之间是可以有父子关系的。如果不设或设为空,则其父协程就是程序默认的”main”主协程。这个”main”协程不需要用户创建,它所对应的方法就是主程序,而所有用户创建的协程都是其子孙。大家可以把greenlet协程集看作一颗树,树的根节点就是”main”,上例中的”gr1″和”gr2″就是其两个字节点。

在子协程执行完毕后,会自动返回父协程。比如上例中test1()函数退出,代码会返回到主程序。让我们写个更清晰的例子来实验下:

这里创建greenlet对象”gr2″时,指定了其父协程是”gr1″。所以在函数test2()里,虽然没有”gr1.switch()”代码,但是在其退出后,程序一样回到了函数test1(),并且执行”print 34″。同样,在test1()退出后,代码回到了主程序,并执行”print 78″。所以,最后的输出就是:

如果上例中,”gr2″的父协程不是”gr1″而是”main”的话,那test2()运行完毕就会回到主程序并直接打印”78″,这样”print 34″就不会执行。大家可以试一试。

还有一个重要的点,就是协程退出后,就无法再被执行了。如果上例在函数test1()中,再加一句”gr2.switch()”,运行的结果是一样的。因为第二次调用”gr2.switch()”,什么也不会运行。

大家可能会感觉到父子协程之间的关系,就像函数调用一样,一个嵌套一个。的确,其实greenlet协程的实现就是使用了栈,其运行的上下文保存在栈中,”main”主协程处于栈底的位置,而当前运行中的协程就在栈顶。这同函数是一样。此外,在任何时候,你都可以使用”greenlet.getcurrent()”,获取当前运行中的协程对象。比如在函数test2()中执行”greenlet.getcurrent()”,其返回就等于”gr2″。

异常

既然协程是存放在栈中,那一个协程要抛出异常,就会先抛到其父协程中,如果所有父协程都不捕获此异常,程序才会退出。我们试下,把上面的例子中函数test2()的代码改为:

程序执行后,我们可以看到Traceback信息:

同时大家可以试下,如果将”gr2″的父协程设为空,Traceback信息就会变为:

因此,如果”gr2″的父协程是”gr1″的话,异常先回抛到函数test1()的代码”gr2.switch()”处。所以,我们再对函数test1()改动下:

运行后的结果,如果”gr2″的父协程是”gr1″,则异常被捕获,并打印90。否则,异常会被抛出。以上实验很好的证明了,子协程抛出的异常会根据栈里的顺序,依次抛到父协程里。

有一个异常是特例,不会被抛到父协程中,那就是”greenlet.GreenletExit”,这个异常会让当前协程强制退出。比如,我们将函数test2()改为:

那代码行”print 78″永远不会被执行。但这个异常不会往上抛,所以其父协程还是可以正常运行。

另外,我们可以通过greenlet对象的”throw()”方法,手动往一个协程里抛个异常。比如,我们在test1()里调一个throw()方法:

这样,异常就会被抛出,运行后的Trackback是这样的:

如果将”gr2.throw(NameError)”放在”try”语句中,那该异常就会被捕获,并打印”90″。另外,当”gr2″的父协程不是”gr1″而是”main”时,异常会直接抛到主程序中,此时函数test1()中的”try”语句就不起作用了。

协程间传递消息

在介绍生成器时,我们聊过可以使用生成器的send()方法来传递参数。greenlet也同样支持,只要在其switch()方法调用时,传入参数即可。我们再来基于本文第一个例子改造下:

在test1()中调用”gr2.switch()”,由于协程”gr2″之前未被启动,所以传入的参数”56″会被赋在test2()函数的参数”x”上;在test2()中调用”gr1.switch()”,由于协程”gr1″之前已执行到第5行”y = gr2.switch(56)”这里,所以传入的参数”34″会作为”gr2.switch(56)”的返回值,赋给变量”y”。这样,两个协程之间的互传消息就实现了。

让我们将上一篇介绍生成器时写的生产者消费者的例子,改为greenlet实现吧:

转自:http://python.jobbole.com/87182/

<think>我们正在讨论Python中的协程。用户想了解协程的使用方法和实现原理。根据提供的引用,我们可以总结如下: 引用[1]介绍了协程Python中实现并发编程的重要方式,比线程更轻量级,适合处理I/O密集型任务。 引用[2]提到gevent是基于greenlet的高级协程库,通过绿色线程和猴子补丁实现自动化协程切换。 引用[3]和[4]展示了使用asyncio库编写协程代码的示例,包括async/await关键字和asyncio.gather的使用。 引用[5]指出协程适合I/O密集型任务,因为在等待I/O时可以释放GIL,不会阻塞线程。 因此,回答将围绕以下几个方面展开: 1. 协程的基础概念:什么是协程,与线程和进程的区别。 2. 协程的实现原理:从生成器到async/await的演进,事件循环机制。 3. 协程的使用方法:使用asyncio库编写协程程序,包括定义协程函数、创建任务、使用asyncio.gather等。 4. 其他协程库的简介:如gevent。 同时,我们将按照系统指令的要求: - 行内数学表达式用$...$(但本问题可能较少涉及数学表达式,若有则按规则处理) - 独立公式用$$...$$(同样,本问题可能不需要) - 使用中文回答 - 生成相关问题 - 回答中引用的段落末尾添加引用标识 另外,回答结构要清晰,逐步解决问题。 下面开始组织回答:</think>### Python协程的使用方法与实现原理 #### 一、协程基础概念 1. **什么是协程** 协程(Coroutine)是一种用户态的轻量级线程,允许在单线程内实现并发执行。与线程不同,协程由程序主动控制切换时机,无需操作系统调度,避免了线程切换的开销[^1]。 核心特点: - 轻量级:协程栈大小仅需几KB,远小于线程MB级开销 - 非抢占式:通过 `yield`/`await` 主动让出执行权 - 高效I/O处理:在I/O等待时自动切换到其他协程 2. **与线程/进程对比** | 特性 | 进程 | 线程 | 协程 | |------------|----------|-----------|-----------| | 切换开销 | 高 | 中 | **极低** | | 内存占用 | 高 | 中 | **极低** | | 并发方式 | 多核并行 | 系统调度 | 用户调度 | | 适用场景 | CPU密集型| 混合任务 | **I/O密集型**[^5] | #### 二、协程实现原理 1. **底层机制** - **事件循环(Event Loop)**:核心调度器,监控所有协程状态 $$ \text{EventLoop} = \{\text{ReadyQueue}, \text{I/OWatcher}, \text{CallbackQueue}\} $$ - **可等待对象(Awaitables)**:协程函数返回的对象,包含挂起/恢复状态 - **Future对象**:承载异步操作结果,通过 `set_result()` 唤醒协程 2. **切换原理** 协程通过保存栈帧上下文实现切换: ```python def coroutine(): context = save_stack_frame() # 保存当前状态 yield # 挂起协程 restore_stack_frame(context) # 恢复执行 ``` 现代Python使用 `async def` 定义的协程会自动生成上下文管理代码[^1]。 #### 三、协程使用方法 1. **基础语法** ```python import asyncio async def fetch_data(url): # 定义协程函数 print(f"Start fetching {url}") await asyncio.sleep(1) # 非阻塞等待 return f"Data from {url}" async def main(): task1 = asyncio.create_task(fetch_data("url1")) # 创建任务 task2 = asyncio.create_task(fetch_data("url2")) results = await asyncio.gather(task1, task2) # 并发执行[^4] print(results) asyncio.run(main()) # 启动事件循环 ``` 2. **关键操作** - **任务创建**:`asyncio.create_task()` 将协程加入事件循环 - **并发控制**:`asyncio.gather()` 或 `asyncio.wait()` - **超时处理**:`await asyncio.wait_for(task, timeout=2.0)` - **同步原语**:`asyncio.Lock()`, `asyncio.Semaphore()` 3. **gevent高级用法** ```python from gevent import monkey; monkey.patch_all() # 猴子补丁替换阻塞调用[^2] import gevent def task(n): print(f"Task {n} start") gevent.sleep(1) # 自动切换协程 print(f"Task {n} done") jobs = [gevent.spawn(task, i) for i in range(3)] gevent.joinall(jobs) # 等待所有协程完成 ``` #### 四、性能对比实验 ```python # I/O密集型任务效率测试(模拟10次网络请求) import asyncio import time import threading # 同步版本 def sync_io(): start = time.time() for _ in range(10): time.sleep(0.1) # 模拟I/O return time.time() - start # 线程版本 def thread_io(): def task(): time.sleep(0.1) start = time.time() threads = [threading.Thread(target=task) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() return time.time() - start # 协程版本 async def async_io(): async def task(): await asyncio.sleep(0.1) start = time.time() await asyncio.gather(*[task() for _ in range(10)]) return time.time() - start # 结果对比 print(f"同步耗时: {sync_io():.3f}s") print(f"线程耗时: {thread_io():.3f}s") print(f"协程耗时: {asyncio.run(async_io()):.3f}s") ``` **典型输出**: ``` 同步耗时: 1.005s 线程耗时: 0.105s 协程耗时: 0.102s ``` 协程在I/O密集型任务中效率接近线程,但资源占用低一个数量级[^5]。 #### 五、应用场景 1. **高并发网络服务**:Web服务器(FastAPI/Sanic)、爬虫 2. **实时数据处理**:流式处理、消息队列消费[^3] 3. **GUI应用**:保持界面响应性 4. **游戏开发**:独立对象行为控制 > **注意**:协程不适合CPU密集型任务,此时应使用多进程避免GIL限制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值