Python中的协程(Coroutine)
Python 中的 协程 (Coroutine) 是一种比线程更加轻量级的并发编程方式。它允许你在单线程内通过“协作”的方式实现多任务并发,特别适合 I/O 密集型 任务(如网络请求、文件读写、数据库操作)。
自 Python 3.5 引入 async 和 await 关键字,以及 Python 3.4 引入标准库 asyncio 以来,协程已经成为 Python 现代并发编程的核心。
以下是关于 Python 协程的详细解析:
1. 核心概念
什么是协程?
- 用户态线程:协程完全由程序(用户态)控制,不由操作系统内核管理。
- 协作式调度:线程是“抢占式”的(操作系统决定什么时候切换),而协程是“协作式”的。协程自己决定何时暂停(挂起)并将控制权交还给事件循环,以便让其他协程运行。
- 单线程并发:Python 的协程通常运行在一个线程中。这意味着同一时刻只有一个协程在执行代码,但利用 I/O 等待的时间切换任务,实现了极高的并发效率。
关键组件
- Event Loop (事件循环):协程的引擎。它是一个无限循环,负责监控 I/O 事件,调度和执行协程。
- Coroutine (协程对象):使用
async def定义的函数调用后返回的对象。 - Task (任务):将协程包装成一个可以在事件循环中调度的对象(包含状态,如 pending, finished)。
- Future:代表一个未来会产生结果的对象(通常由底层库使用)。
2. 基本语法 (async / await)
这是 Python 3.5+ 的标准写法。
async def:定义一个协程函数。await:挂起当前协程,等待后面的对象(Awaitable)执行完毕,并交出控制权。
最简单的例子 (Hello World)
python
import asyncio
import time
# 定义协程函数
async def say_after(delay, what):
# await 后面必须跟一个可等待对象(如另一个协程)
# asyncio.sleep 是非阻塞的睡眠,模拟 I/O 操作
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
# 串行执行(这不是并发)
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
# 运行顶层入口点
if __name__ == "__main__":
# asyncio.run() 会自动创建事件循环并运行
asyncio.run(main())
输出结果(耗时约 3 秒):
plaintext
started at 12:00:00
hello
world
finished at 12:00:03
3. 如何实现并发?
上面的例子中,await 会等待前一个任务完成才执行下一个,这实际上是串行的。要实现并发(同时等待多个任务),需要使用 asyncio.create_task() 或 asyncio.gather()。
并发执行示例
python
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
# 创建任务(Task),这会将协程立即调度到事件循环中运行
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# 等待两个任务都完成
await task1
await task2
print(f"finished at {time.strftime('%X')}")
if __name__ == "__main__":
asyncio.run(main())
输出结果(耗时约 2 秒,取决于最长的那个任务):
plaintext
started at 12:00:00
hello (1秒后出现)
world (2秒后出现)
finished at 12:00:02
原理:task1 遇到 sleep 挂起,事件循环立即切换去执行 task2,task2 也挂起,事件循环等待。1秒后 task1 恢复,2秒后 task2 恢复。
4. 协程 vs 线程 vs 进程
| 特性 | 进程 (Process) | 线程 (Thread) | 协程 (Coroutine) |
|---|---|---|---|
| 内存占用 | 极大 (独立内存空间) | 中等 (共享进程内存) | 极小 (几 KB) |
| 切换开销 | 极大 (内核态) | 大 (内核态) | 极小 (用户态) |
| 并发能力 | 低 (受限于 CPU 核数) | 中 (受限于系统资源) | 极高 (单线程可支持数万协程) |
| 数据安全 | 安全 (内存隔离) | 不安全 (需加锁) | 相对安全 (非抢占式,无竞态条件) |
| 适用场景 | CPU 密集型计算 | I/O 密集型 (传统) | 高并发 I/O 密集型 |
5. 常见误区与注意事项
1. 即使写了 async,代码也可能是阻塞的
绝对不能在协程中使用同步的阻塞 I/O,否则整个线程(包括事件循环)都会卡住,其他协程也无法运行。
❌ 错误做法:
pythonimport time import requests # requests 库是同步阻塞的 async def bad_coroutine(): # 这会卡死整个程序5秒,期间其他协程无法运行 time.sleep(5) # 这也会卡死 requests.get("http://google.com")✅ 正确做法:
使用异步库替代同步库。time.sleep->asyncio.sleeprequests->aiohttp或httpx(异步模式)pymysql->aiomysql
2. CPU 密集型任务怎么办?
协程本质是单线程。如果你在协程里跑一个死循环计算(如计算圆周率),整个 Loop 都会被卡住。
解决方案:使用 run_in_executor 将 CPU 密集型任务扔到线程池或进程池中去跑。
python
import asyncio
import time
# 一个耗时的同步函数
def blocking_io():
print(f"start blocking_io at {time.strftime('%X')}")
time.sleep(1) # 模拟阻塞
print(f"blocking_io complete at {time.strftime('%X')}")
async def main():
loop = asyncio.get_running_loop()
# 在默认的线程池执行器中运行阻塞函数
# 这样不会阻塞主线程的事件循环
await loop.run_in_executor(None, blocking_io)
asyncio.run(main())
6. 实战:异步爬虫示例 (使用 aiohttp)
这是一个典型的协程应用场景:同时请求多个网页。
python
import asyncio
import aiohttp
import time
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'http://www.python.org',
'http://www.google.com',
'http://www.baidu.com'
]
async with aiohttp.ClientSession() as session:
tasks = []
for url in urls:
# 创建任务列表
task = asyncio.create_task(fetch(session, url))
tasks.append(task)
# gather 并发运行所有任务并收集结果
results = await asyncio.gather(*tasks)
for url, html in zip(urls, results):
print(f"Read {len(html)} chars from {url}")
if __name__ == "__main__":
start = time.time()
# 注意:运行此代码需要安装 aiohttp (pip install aiohttp)
# 某些环境(如 Jupyter)可能已有运行中的 loop,需特殊处理,
# 标准脚本中直接用 asyncio.run(main()) 即可。
try:
asyncio.run(main())
except RuntimeError:
# 针对 Jupyter Notebook 等环境的兼容写法
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
print(f"Cost: {time.time() - start:.2f} seconds")
总结
- 协程是单线程的,通过事件循环在 I/O 等待时切换任务。
- 关键字:
async def定义,await调用,asyncio.run启动。 - 核心优势:极高的 I/O 并发性能,资源消耗低。
- 核心禁忌:不要在协程中调用同步阻塞代码(如
time.sleep或requests),必须使用对应的异步库。
右滑查看面试常问