在 Python 中,协程(Coroutine)是一种比线程更轻量级的并发执行方式,它允许在程序中暂停执行某个任务并切换到其他任务,从而提高程序的效率,尤其适用于 I/O 密集型任务。
协程的核心概念:
- 协程是可挂起的函数,它能够在执行过程中暂停(挂起),并且能够在之后恢复执行。
- 协程是 基于事件循环 和 异步 I/O 模型来实现并发的,而不是通过多线程或多进程。
- 协程通常与 async 和 await 关键字一起使用。
1. 协程的定义
在 Python 中,协程是通过 async def 关键字定义的。这种函数定义会返回一个协程对象,而不是直接执行函数的代码。
import asyncio
# 定义一个协程
async def my_coroutine():
print("协程开始")
await asyncio.sleep(1) # 模拟耗时操作,非阻塞
print("协程结束")
在上面的例子中:
- async def 定义了一个异步函数 my_coroutine。
- await asyncio.sleep(1) 是一个非阻塞的 sleep,它让协程等待 1 秒钟,不会阻塞整个程序。
2. await 关键字
await 是协程中用于暂停执行当前协程的关键字。它会等待另一个协程或异步操作完成,并且在等待的过程中可以执行其他任务(如其他协程)。
async def example():
print("开始等待")
await asyncio.sleep(2) # 暂停 2 秒
print("等待结束")
3. 事件循环(Event Loop)
Python 的异步编程依赖于事件循环(event loop)。事件循环是一个不断轮询的循环,负责管理和调度任务。协程通过事件循环进行调度,当一个协程被 await 挂起时,事件循环可以去执行其他的协程。
4. 运行协程
协程并不会立即执行,必须通过 asyncio 或类似的异步运行机制来运行它。最常见的做法是使用 asyncio.run() 来启动协程。
# 运行协程
asyncio.run(my_coroutine())
5. 多个协程并发执行
可以同时运行多个协程,通过 asyncio.gather() 来并发执行多个协程。
async def task1():
print("任务 1 开始")
await asyncio.sleep(1)
print("任务 1 结束")
async def task2():
print("任务 2 开始")
await asyncio.sleep(2)
print("任务 2 结束")
async def main():
# 并发运行任务 1 和 任务 2
await asyncio.gather(task1(), task2())
# 运行主协程
asyncio.run(main())
6. asyncio.run() 的作用
asyncio.run() 用于启动一个新的事件循环并运行给定的协程。在事件循环完成后,asyncio.run() 会关闭事件循环并返回。具体而言,asyncio.run() 会执行以下操作:
- 创建一个新的事件循环。
- 在该事件循环中运行传入的协程,直到协程执行完成。
- 关闭事件循环。
为什么说 asyncio.run() 是阻塞的?
当你调用 asyncio.run() 时,它会启动并运行一个事件循环,直到事件循环中的所有任务完成。这个过程是 阻塞的,也就是说,asyncio.run() 会等到传入的协程执行完毕后才会返回。
import asyncio
async def my_coroutine():
print("协程开始")
await asyncio.sleep(1)
print("协程结束")
# 事件循环在这里会被阻塞,直到协程执行完毕
asyncio.run(my_coroutine())
print("主程序结束")
输出:
协程开始
协程结束
主程序结束
7. 协程的优势
- 轻量级:协程在内存和执行开销上比线程更轻量。相比线程,它们不需要上下文切换和线程间通信,因此可以高效处理大量并发任务。
- 异步 I/O 操作:协程特别适合处理 I/O 密集型任务,例如网络请求、磁盘操作、数据库查询等。这些任务通常涉及等待外部资源,协程可以在等待过程中执行其他任务,而不像传统的同步方式那样阻塞整个程序。
- 无需多线程:协程通过事件循环机制来实现并发,不需要额外的线程管理,避免了线程创建和上下文切换的开销。
8. 协程的与线程的对比
- 线程:每个线程都有自己的栈内存,操作系统需要为其分配资源(如线程上下文)。线程之间的切换是由操作系统进行的,通常会有比较高的性能开销。
- 协程:协程比线程更加轻量,内存消耗较小,且协程之间的切换由 Python 解释器管理,因此切换开销小很多。协程适用于 I/O 密集型任务,而线程适用于 CPU 密集型任务。
9. 协程的应用场景
- 网络请求:通过协程,可以在等待网络响应时执行其他任务,从而提高效率。
数据库查询:在数据库操作过程中,可以使用协程执行其他查询。 - 爬虫:爬虫程序通常需要等待 HTTP 请求的响应,协程可以在等待时并发处理其他请求。
GUI 程序:在一些 GUI 程序中,可以使用协程在后台执行耗时任务,同时保持界面响应。