|
| 1 | ++++ |
| 2 | +date = '2025-08-29T8:00:00+08:00' |
| 3 | +draft = false |
| 4 | +title = 'Asyncio vs Gevents in Python' |
| 5 | +tags = ['Python', 'Concurrency'] |
| 6 | ++++ |
| 7 | + |
| 8 | +python 中 asyncio 和 gevent 是两种协程(在一个线程内实现并发)的实现, 这篇文章对比介绍这两者实现. |
| 9 | +下面先介绍一下基础概念: |
| 10 | + |
| 11 | +### Coroutines 协程 |
| 12 | +在 Python 中, 协程是可以暂停和继续运行的函数, 使得其是否适合并发编程. 定义使用 `async def` 语法, 协程运行编写非阻塞的操作. 在协程内, `await` 关键字用于暂停执行, 直到给定的任务完成, 从而运行其他协程在此其间并发运行. |
| 13 | + |
| 14 | +### Event Loop 事件循环 |
| 15 | +事件循环是一种控制结构, 它不断地处理一系列事件, 处理任务并管理程序的执行流程. 等待事件发生, 处理后再等待下一个事件. 这种机制确保程序能够以高效有序的方式响应事件, 例如用户输入、计时器或者消息. |
| 16 | + |
| 17 | +下面是事件循环如何管理协程: |
| 18 | +- 任务提交: 当向事件循环提交一个协程时, 其被封装在一个 `Task` 对象中, 然后任务被安排在事件循环上运行. |
| 19 | +- 内部队列: 事件循环使用几个内部数据结构来管理和调度这些任务 |
| 20 | + |
| 21 | + - 就绪队列 (Ready Queue): 包含可以立即运行的任务. |
| 22 | + - I/O 选择器 (I/O Selector): 监控文件描述符, 并根据 I/O 准备情况调度任务 |
| 23 | + - 计划回调 (Scheduled Callbacks): 管理计划在一定延迟后运行的任务. |
| 24 | + |
| 25 | +- 调度: 事件循环不断检查这些队列和数据结构, 以确定哪些任务已准备好执行. 然后它运行这些任务, 在遇到 await 语句时, 根据需要暂停和恢复它们. |
| 26 | +- 并发管理: 通过交错执行多个协程, 事件循环无需多个线程即可实现并发. 在任何时候, 只有一个任务会运行, 但如果一个任务是 I/O 密集型的, 它会切换到另一个任务, 给人一种并行的错觉. |
| 27 | + |
| 28 | +### Asyncio In Action |
| 29 | +```Python |
| 30 | +import asyncio |
| 31 | +import time |
| 32 | + |
| 33 | +async def task1(): |
| 34 | + print("Task 1 started") |
| 35 | + await asyncio.sleep(1) # 将控制权让给事件循环 |
| 36 | + print("Task 1 resumed") |
| 37 | + await asyncio.sleep(1) # 将控制权让给事件循环 |
| 38 | + |
| 39 | +async def task2(): |
| 40 | + print("Task 2 started") |
| 41 | + await asyncio.sleep(1) # 将控制权让给事件循环 |
| 42 | + print("Task 2 resumed") |
| 43 | + await asyncio.sleep(1) # 将控制权让给事件循环 |
| 44 | + |
| 45 | +async def main(): |
| 46 | + await asyncio.gather(task1(), task2()) |
| 47 | + |
| 48 | +start_time = time.time() |
| 49 | +asyncio.run(main()) |
| 50 | +end_time = time.time() |
| 51 | + |
| 52 | +print(f"Total time: {end_time - start_time:.2f} seconds") |
| 53 | + |
| 54 | +''' |
| 55 | +任务 1 启动,并使用 await asyncio.sleep(1) 让出控制权。 |
| 56 | +任务 2 启动,并使用 await asyncio.sleep(1) 让出控制权。 |
| 57 | +1 秒后,两个任务都恢复。 |
| 58 | +任务 1 恢复,并使用 await asyncio.sleep(1) 让出控制权。 |
| 59 | +任务 2 恢复,并使用 await asyncio.sleep(1) 让出控制权。 |
| 60 | +又过了 1 秒,两个任务都完成。 |
| 61 | +总耗时为 2 秒。 |
| 62 | +''' |
| 63 | +``` |
| 64 | +通过上面的例子, 可以看到如何在进行 I/O 操作时通过切换任务来获得好处. 同样的逻辑如果按顺序执行需要 4 秒, 但使用 asyncio,可以将时间缩短一半. |
| 65 | +在提供的代码中, 事件循环就像一个在单个线程上运行的管理器. 它跟踪 task1 和 task2 这样的任务, 确保它们轮流运行. |
| 66 | +CPU 逐一处理这些任务, 但当一个任务等待某事时(例如使用 `await asyncio.sleep` 暂停), 它会将控制权交给事件循环. |
| 67 | +这使得事件循环可以切换到另一个准备好运行的任务. |
| 68 | +这样, 即使所有事情都在一个线程中发生, 任务也能高效且并发地执行, 而无需等待彼此完全完成. |
| 69 | + |
| 70 | +**Asyncio 术语** |
| 71 | + |
| 72 | +- `asyncio.run(coro)` |
| 73 | + - 运行主协程 `coro` 并管理事件循环, 创建一个新的事件循环, 运行协程直到完成, 然后关闭循环 |
| 74 | + - 被设计用于异步函数外部, 通常在程序的入口点调用 |
| 75 | + - 不能在已存在的事件循环内部运行 |
| 76 | + |
| 77 | +- `asyncio.create_task(coro)` |
| 78 | + - 安排协程 `core` 并发运行, 并返回一个 `Task` 对象, 这个函数对启动多个协程非常有用 |
| 79 | + - 此命令需要一个已存在的事件循环才能执行 |
| 80 | + - 用于启动一个应该与其他任务并发运行的协程, 非常适合需要与其他异步操作并行运行的任务 |
| 81 | + |
| 82 | +- `asyncio.gather(*coros)` |
| 83 | + - 并发运行多个协程并等待它们全部完成, 它将它们的结果收集到一个列表中 |
| 84 | + - 需要一个活动的事件循环来管理协程 |
| 85 | + |
| 86 | +- `event_loop.run_untill_complete(core)` |
| 87 | + - 使用已存在的事件循环运行协程, 直到完成. 会阻塞直到协程完成并返回结果. |
| 88 | + - 不应该在异步函数内部使用, 它旨在运行协程直到其完成, 应在异步函数外部使用, 通常在同步上下文中 |
| 89 | + |
| 90 | + |
| 91 | +### Greenlets 和 Gevent |
| 92 | +Coroutines 协程 和 greenlets(green threds 绿色线程) 都是管理并发执行的方法, 但它们在实现、控制和使用场景方面有明显的区别. |
| 93 | +Greenlets 是由 Python 的 greenlet 库提供的低级、用户空间协程实现 |
| 94 | +```Python |
| 95 | +from greenlet import greenlet |
| 96 | +import time |
| 97 | + |
| 98 | +def task1(): |
| 99 | + start_time = time.time() |
| 100 | + print("Task 1 started") |
| 101 | + time.sleep(1) # 模拟工作 |
| 102 | + print("Task 1 yielding") |
| 103 | + gr2.switch() # 将控制权让给 task2 |
| 104 | + print("Task 1 resumed") |
| 105 | + time.sleep(1) # 模拟更多工作 |
| 106 | + end_time = time.time() |
| 107 | + print(f"Task 1 completed in {end_time - start_time:.2f} seconds") |
| 108 | + |
| 109 | +def task2(): |
| 110 | + start_time = time.time() |
| 111 | + print("Task 2 started") |
| 112 | + time.sleep(1) # 模拟工作 |
| 113 | + print("Task 2 yielding") |
| 114 | + gr1.switch() # 将控制权让给 task1 |
| 115 | + print("Task 2 resumed") |
| 116 | + time.sleep(1) # 模拟更多工作 |
| 117 | + end_time = time.time() |
| 118 | + print(f"Task 2 completed in {end_time - start_time:.2f} seconds") |
| 119 | + |
| 120 | +# 创建 greenlets |
| 121 | +gr1 = greenlet(task1) |
| 122 | +gr2 = greenlet(task2) |
| 123 | + |
| 124 | +# 启动 task1 并切换到 task2 |
| 125 | +start_time = time.time() |
| 126 | +gr1.switch() |
| 127 | +gr2.switch() |
| 128 | +end_time = time.time() |
| 129 | + |
| 130 | +print(f"Total execution time: {end_time - start_time:.2f} seconds") |
| 131 | + |
| 132 | +''' |
| 133 | +Task 1 started |
| 134 | +Task 1 yielding |
| 135 | +Task 2 started |
| 136 | +Task 2 yielding |
| 137 | +Task 1 resumed |
| 138 | +Task 1 completed in 3.01 seconds |
| 139 | +Task 2 resumed |
| 140 | +Task 2 completed in 3.01 seconds |
| 141 | +Total execution time: 4.02 seconds |
| 142 | +''' |
| 143 | +``` |
| 144 | +Greenlet 在协作式多任务处理方式中为用户提供了完全的灵活性, 可以切换不同的执行上下文, 但它缺乏对异步 I/O 操作的内置支持. |
| 145 | + |
| 146 | +Gevent 是一个构建在 Greenlet 之上的更高级的库, 提供对非阻塞 I/O 的内置支持和更高级的抽象, 适用于 I/O 密集型应用. |
| 147 | +Gevent 抽象了上下文切换的复杂性, 并提供了对非阻塞 I/O 操作的内置支持. |
| 148 | +```Python |
| 149 | +import gevent |
| 150 | +import time |
| 151 | + |
| 152 | +def task1(): |
| 153 | + print("Task 1 started") |
| 154 | + gevent.sleep(1) |
| 155 | + print("Task 1 resumed") |
| 156 | + gevent.sleep(1) |
| 157 | + |
| 158 | +def task2(): |
| 159 | + print("Task 2 started") |
| 160 | + gevent.sleep(1) |
| 161 | + print("Task 2 resumed") |
| 162 | + gevent.sleep(1) |
| 163 | + |
| 164 | +start_time = time.time() |
| 165 | + |
| 166 | +# 创建 greenlets |
| 167 | +g1 = gevent.spawn(task1) |
| 168 | +g2 = gevent.spawn(task2) |
| 169 | + |
| 170 | +# 启动 greenlets 并等待它们完成 |
| 171 | +gevent.joinall([g1, g2]) |
| 172 | + |
| 173 | +end_time = time.time() |
| 174 | + |
| 175 | +print(f"Total time: {end_time - start_time:.2f} seconds") |
| 176 | + |
| 177 | +''' |
| 178 | +Task 1 started |
| 179 | +Task 2 started |
| 180 | +Task 1 resumed |
| 181 | +Task 2 resumed |
| 182 | +Total time: 2.03 seconds |
| 183 | +''' |
| 184 | +``` |
| 185 | + |
| 186 | +下面对比 gevent 和 asyncio : |
| 187 | + |
| 188 | +- 事件循环管理 |
| 189 | + - Gevent: 管理自己的事件循环, 并依赖猴子补丁(monkey patching)使标准 I/O 操作变为异步. 这意味着它会修改标准库模块的行为以支持其并发模型 |
| 190 | + - Asyncio: 包含一个内置事件循环, 它是 Python 标准库的一部分. 提供对管理异步操作的原生支持, 无需猴子补丁 |
| 191 | + |
| 192 | +- 猴子补丁 |
| 193 | + - Gevent: 需要显式猴子补丁来将阻塞的 I/O 操作转换为非阻塞的. 这涉及修改标准库模块以与 gevent 的事件循环集成 |
| 194 | + - Asyncio: 不需要猴子补丁, 它使用 Python 原生的 async/await 功能, 该功能与标准库的异步 I/O 操作无缝集成 |
| 195 | + |
| 196 | +- 性能 |
| 197 | + - Gevent: 对于 I/O 密集型任务是高效的, 特别是在已经使用猴子补丁的系统中. 由于需要猴子补丁, 它可能会增加开销, 但在许多场景下仍然有效 |
| 198 | + - Asyncio: 通常通过对异步编程的原生支持提供高性能. 它针对现代应用进行了优化, 并提供高效的 I/O 密集型任务处理, 没有猴子补丁的开销 |
| 199 | + |
| 200 | +- 错误处理 |
| 201 | + - Gevent: 由于使用了猴子补丁的库, 可能需要仔细管理异常. 错误处理需要在 greenlets 内部进行管理 |
| 202 | + - Asyncio: 在协程中利用标准的 Python 错误处理, 原生的语法使得在异步代码中处理异常更容易 |
| 203 | + |
| 204 | +- 使用场景 |
| 205 | + - Gevent: 非常适合将异步行为集成到现有的同步代码库中, 或与兼容 greenlets 的库一起工作, 它适用于需要将现有 I/O 操作打补丁为异步的应用 |
| 206 | + - Asyncio: 最适合采用现代异步编程实践的新应用或代码库, 它非常适合高性能网络应用和 I/O 密集型任务, 这些任务从原生异步支持中受益 |
| 207 | + |
| 208 | + |
| 209 | +Asyncio 通常是新应用的首选, 因为它倾向于现代异步编程实践, 它与 Python 的标准库无缝集成, 非常适合网络应用、实时通信和需要高并发的服务. |
| 210 | +Gevent 通常是现有同步代码库的首选, 这些代码库需要进行改造以支持并发, 它能够对标准库模块进行猴子补丁, 使其非常适合需要将阻塞的 I/O 操作转换为非阻塞的应用, 例如在网络服务器、聊天应用和实时系统中. |
| 211 | + |
| 212 | +### Examples |
| 213 | +下面是一些使用 asyncio 和 gevent 的例子 |
| 214 | + |
| 215 | +- Web Servers |
| 216 | + - FastAPI: 虽然主要基于 Starlette 和 Pydantic 构建, 但 FastAPI 利用 asyncio 来处理异步请求, 使其成为一个用于构建 API 的高性能 Web 框架 |
| 217 | + - Gunicorn with gevent workers: 一个流行的 Python 应用 WSGI HTTP 服务器, 可以使用 gevent workers 来高效地处理大量并发连接 |
| 218 | + - Flask with gevent: 尽管 Flask 本身是同步的, 但将其与 gevent 结合可以并发处理多个请求, 使其适用于实时应用 |
| 219 | + |
| 220 | +- 实时通信 |
| 221 | + - Discord.py: 一个 Discord 的 API 封装库, 它使用 asyncio 来高效地处理实时事件和交互 |
| 222 | + |
| 223 | +- 网络工具 |
| 224 | + - AsyncSSH: 一个用于 SSHv2 协议实现的库, 在 asyncio 之上构建, 为使用 SSH、SFTP 和 SCP 提供了异步 API |
| 225 | + - ZeroMQ with gevent: 对于需要高性能消息传递的应用, gevent 经常与 ZeroMQ 一起使用, 以有效地处理异步通信模式 |
| 226 | + |
| 227 | +- 数据库访问 |
| 228 | + - Gevent with SQLAlchemy: 对于需要异步数据库访问的应用, 将 gevent 与 SQLAlchemy 结合可以处理数据库查询而不会阻塞主线程 |
| 229 | + |
| 230 | +### Wrapping Up |
| 231 | +总而言之, asyncio 和 gevent 都提供了在 Python 中实现并发的强大工具, 但它们满足不同的需求和使用场景. |
| 232 | +Asyncio 是新应用的绝佳选择, 它利用了 Python 的原生异步能力, 而 gevent 则擅长将异步行为集成到现有的同步代码库中, 尤其是在处理 I/O 密集型任务时. |
| 233 | +具体使用哪种还是要根据不同的开发环境判断. |
0 commit comments