Python 协程的两种核心实现:生成器与原生协程对比
在 Python 中,协程(Coroutine)是一种轻量级的并发编程方式,通过协作式多任务来实现高效的并发执行。协程允许程序在单线程内并发处理多个任务,而无需承担多线程上下文切换和锁机制带来的开销。
本文详细解析了 Python 中协程的两种核心实现方式:生成器协程与原生协程。生成器协程基于 yield 关键字,兼容性好但需手动管理启动;原生协程基于 async/await 关键字,语法简洁且性能更优,支持标准异常处理。文章对比了两者的定义、调用及优缺点,并通过批量异步任务处理的实战案例展示了原生协程在并发控制与错误处理中的应用。最后总结了常见误区与最佳实践,指导开发者选择合适的协程方案以提升程序效率。

在 Python 中,协程(Coroutine)是一种轻量级的并发编程方式,通过协作式多任务来实现高效的并发执行。协程允许程序在单线程内并发处理多个任务,而无需承担多线程上下文切换和锁机制带来的开销。
协程是一种特殊的函数,它可以在执行过程中暂停并保存当前状态,稍后从暂停处恢复执行。这种机制使得协程非常适合处理 I/O 密集型任务,如网络请求、文件读写等。
相比于线程和进程,协程具有以下显著优点:
协程的典型使用场景包括网络编程、异步 I/O、数据流处理、高并发任务调度等。
在 Python 3.4 之前,协程主要通过生成器函数来实现,称为'生成器协程'。生成器函数是一种特殊的函数,其返回一个生成器对象,可以通过 yield 语句暂停函数的执行,然后在下一次调用生成器对象的 next() 方法或 send() 方法时继续执行。
import asyncio
def coroutine():
print('Coroutine started')
while True:
result = yield
print('Coroutine received:', result)
async def main():
print('Main started')
c = coroutine()
next(c) # 启动生成器,执行到第一个 yield
c.send('Hello') # 发送消息并恢复执行
await asyncio.sleep(1) # 等待 1 秒
c.send('World') # 再次发送消息
print('Main finished')
asyncio.run(main())
main 函数开始执行,打印出 Main started。c,调用 next(c) 使其执行到第一个 yield 语句处暂停。c.send('Hello') 恢复生成器函数的执行,并将 'Hello' 作为生成器函数的返回值。main 函数暂停执行,等待事件循环发起下一次任务。c.send('World') 继续执行生成器函数,并将 'World' 作为生成器函数的返回值。main 函数恢复执行,打印出 Main finished。在上面的代码中,生成器函数通过使用 yield 语句暂停函数的执行,然后通过 send 方法恢复函数的执行,并将值传递给生成器函数。这种方式虽然能实现异步并发,但在 Python 3.5 引入原生协程之前,需要手动管理生成器的启动和唤醒,较为繁琐。
Python 3.5 引入了原生协程(Native Coroutine),这是一种新的协程类型。原生协程是通过使用 async/await 关键字来定义的,与生成器协程不同,它们可以像普通函数一样使用 return 语句返回值,而不是使用 yield 语句。
import asyncio
async def coroutine():
print('Coroutine started')
await asyncio.sleep(1)
print('Coroutine finished')
async def main():
print('Main started')
await coroutine()
print('Main finished')
asyncio.run(main())
main 函数开始执行,打印出 Main started。coroutine 函数,将其作为一个协程对象运行。coroutine 函数中,打印出 Coroutine started。coroutine 函数中,使用 await asyncio.sleep(1) 暂停函数的执行,等待 1 秒钟。coroutine 函数的执行,并打印出 Coroutine finished。main 函数恢复执行,打印出 Main finished。在上面的代码中,使用 async 关键字定义了一个原生协程函数,并在其中使用 await 关键字来暂停函数的执行,等待异步 I/O 操作的完成。通过这种方式,可以在原生协程中编写异步并发代码,从而提高代码的性能和效率。
Python 中原生协程和生成器协程是两种不同的协程实现方式,它们各自有自己的特点和适用场景。
async/await 关键字来定义,而生成器协程使用 yield 关键字来定义。return 语句来返回结果,而生成器协程使用 yield 语句来返回结果。await 关键字来调用,而生成器协程使用 yield from 或 send 语句来调用。asyncio 库来实现,而生成器协程是 Python 语言内置的特性。优点:
async/await 关键字,可以编写出更简洁易懂的协程代码,逻辑更接近同步代码。yield 语句来控制函数的执行流程,因此能够更加高效地处理异步操作。try-except 异常捕获机制,错误处理更加直观。缺点:
优点:
yield 关键字来实现,代码逻辑清晰易懂,适合简单的数据流处理。try-except 语句来处理。缺点:
yield 语句来控制函数的执行流程,因此处理异步操作时性能相对较低。asyncio 手动管理。next() 或 send() 来启动,容易遗漏导致协程未执行。接下来,模拟一个场景,假设实现一个异步的批量处理任务的工具,使用原生协程来实现。这个案例将展示如何处理并发、批次控制以及异常捕获。
import asyncio
import random
import time
async def process_task(task_id):
"""
模拟单个任务的处理过程
"""
try:
# 模拟耗时操作,随机睡眠 0.5 到 2.0 秒
delay = random.uniform(0.5, 2.0)
await asyncio.sleep(delay)
print(f"Task {task_id} processed in {delay:.2f}s")
return task_id
except Exception as e:
print(f"Task {task_id} failed: {e}")
raise
async def batch_process_task(tasks, batch_size=10):
"""
将任务列表划分为多个批次进行并发处理
"""
start_time = time.time()
for i in range(0, len(tasks), batch_size):
batch = tasks[i:i+batch_size]
# 使用 asyncio.gather 并发处理每个批次的任务
# limit 参数限制最大并发数,防止资源耗尽
await asyncio.gather(*[process_task(task) for task in batch])
end_time = time.time()
print(f"All tasks completed in {end_time - start_time:.2f}s")
async def main():
# 构造任务列表,模拟 100 个任务
tasks = [i for i in range(1, 101)]
# 并发处理批量任务,每批 10 个
await batch_process_task(tasks, batch_size=10)
if __name__ == '__main__':
asyncio.run(main())
运行上述代码,控制台会输出类似以下的日志(顺序可能因系统调度而异):
Task 9 processed in 1.23s
Task 10 processed in 0.85s
Task 1 processed in 1.50s
...
All tasks completed in 5.42s
在这个案例中,batch_process_task 函数使用原生协程来处理每个批次的任务,process_task 函数则是处理每个具体任务的函数。main 函数负责构造任务列表,并且使用 batch_process_task 函数来异步地处理批量任务。通过 asyncio.gather,我们实现了在同一批次内的任务并发执行,同时通过分批处理控制了并发度,避免了对系统资源的过度消耗。
在使用 Python 协程时,开发者需要注意以下几点以避免常见错误:
time.sleep 或 requests.get),会导致整个事件循环被阻塞,失去并发意义。应始终使用 await asyncio.sleep 或 aiohttp 等异步库。try-except 块捕获,或者让 asyncio.gather 收集异常。未处理的异常可能导致协程静默失败。asyncio.Semaphore 或 gather 的 limit 参数来控制并发数量。Python 提供了两种主要的协程实现方式:生成器协程和原生协程。生成器协程基于 yield 关键字,兼容性好但功能受限;原生协程基于 async/await 关键字,语法更现代,性能更优,是 Python 3.5+ 推荐的标准写法。在实际开发中,建议优先使用原生协程配合 asyncio 框架,以实现高效、可维护的异步程序。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online