Python 中的协程,是一种用于协作式多任务处理的程序组件。它允许你在单个线程内,通过主动让出控制权来实现并发,特别适合处理大量 I/O 密集型任务,可以避免传统多线程带来的上下文切换开销和复杂的锁问题。
下面我们从概念到实践,系统地讲解 Python 协程。
一、什么是协程
- 常规函数:调用后一口气执行完毕,期间不会主动暂停。
- 协程函数 :执行过程中可以暂停并保存当前状态,把控制权交还给调度器,之后再从暂停的地方恢复执行。
这种"自己让出执行权"的方式就叫做协作式。它不同于线程那种由操作系统强制切换的抢占式多任务。
Python 从 yield 生成器演化而来,最终在 3.5 引入了 async def / await 语法,正式确立了原生协程。
二、快速体验
python
import asyncio
async def hello():
print("Hello")
await asyncio.sleep(1) # 模拟 I/O,主动让出控制权
print("World")
asyncio.run(hello())
async def定义一个协程函数,调用它并不会立刻执行,而是返回一个协程对象。await用来等待另一个协程或可等待对象,期间挂起当前协程。asyncio.sleep(1)是一个异步的休眠,不会阻塞整个线程。asyncio.run()用来运行顶层协程并管理事件循环。
三、核心概念
3.1 可等待对象(Awaitable)
能在 await 后面使用的对象有三种:
- 协程对象 :
async def函数的返回值。 - 任务(Task) :用
asyncio.create_task()包装协程,可以并发调度。 - Future:底层占位符,代表一个未来才会完成的结果(Task 是一种 Future)。
python
async def nested():
return 42
async def main():
# 直接 await 协程
print(await nested())
# 包装成任务,立即开始并发执行
task = asyncio.create_task(nested())
print(await task)
asyncio.run(main())
3.2 事件循环(Event Loop)
事件循环是协程的调度核心,它不断地检查哪些任务可以继续执行,循环往复。asyncio.run() 会创建、运行并清理事件循环。在单线程里,所有协程都由这个循环统一调度。
四、并发执行
同时发起多个 I/O 操作,等其全部完成,这正是协程的优势。
python
import asyncio
import time
async def download(name, duration):
print(f"开始下载 {name}")
await asyncio.sleep(duration) # 模拟下载耗时
print(f"{name} 下载完成")
return f"{name} 的数据"
async def main():
start = time.time()
# 依次执行,约 4 秒
# await download("文件1", 2)
# await download("文件2", 2)
# 并发执行,约 2 秒
results = await asyncio.gather(
download("文件1", 2),
download("文件2", 2),
)
print(results)
print(f"总耗时 {time.time() - start:.2f} 秒")
asyncio.run(main())
asyncio.gather() 会并发等待多个协程,全部完成后返回结果列表。
若某个协程失败,可以通过 return_exceptions=True 让异常不会被立即抛出,而是作为结果返回。
五、创建与管理任务
asyncio.create_task() 会将协程包装成 Task,并立刻注册到事件循环中执行。你可以在稍后等待它。
python
async def fire_and_forget():
task = asyncio.create_task(download("文件3", 3))
# 这里可以做其他事,task 在后台运行
await asyncio.sleep(0.1)
# 再等待 task
await task
常用任务管理方法:
task.cancel()取消任务(会引发CancelledError)。asyncio.wait_for(aw, timeout)给协程加超时限制。asyncio.shield(aw)保护协程不被外部取消。
六、异步生成器与异步迭代器
在 async def 里使用 yield 就得到异步生成器,用 async for 遍历。
python
import asyncio
async def countdown(n):
while n > 0:
await asyncio.sleep(1)
yield n
n -= 1
async def main():
async for num in countdown(3):
print(num)
asyncio.run(main())
异步生成器内部可以包含 await,每次 yield 前都可以挂起,非常适合流式处理异步数据源。
异步推导式:
python
result = [x async for x in async_iter]
result = {x async for x in async_iter}
result = {key: val async for key, val in async_items}
七、同步原语与队列
asyncio 提供了协程版本的锁、信号量、事件和条件等,用于保护共享资源或协调任务。
python
lock = asyncio.Lock()
async def safe_update(data):
async with lock:
# 修改共享状态
pass
asyncio.Queue 可用于生产者-消费者模式:
python
queue = asyncio.Queue(maxsize=10)
async def producer():
for i in range(5):
await queue.put(i)
await asyncio.sleep(0.5)
async def consumer():
while True:
item = await queue.get()
print(f"消费 {item}")
queue.task_done()
八、与线程的交互
协程在单线程里运行,但有时需要调用已有的同步阻塞库。asyncio.to_thread() 可以将阻塞函数扔到线程池中异步执行。
python
import time
import asyncio
def blocking_io():
time.sleep(1)
return "结果"
async def main():
result = await asyncio.to_thread(blocking_io)
print(result)
asyncio.run(main())
loop.run_in_executor() 则是更底层的接口,同样可以指定线程或进程池。
九、常见陷阱与最佳实践
-
async函数里不要用同步阻塞调用例如
time.sleep()会卡住整个事件循环,应使用await asyncio.sleep()。 -
不要忘记
await任务如果只创建了
create_task()但从不对它await,可能看不到异常,且程序可能提前退出。最好保存 task 引用,并在最后等待。 -
避免混用
asyncio.run()在已有事件循环的环境(如 Jupyter Notebook,或部分 Web 框架)里反复调用会出错,可以用
await代替。 -
并发度控制
如果同时发起成千上万个请求,可以使用
asyncio.Semaphore限制并发数,防止对方服务器过载或本地资源耗尽。pythonsem = asyncio.Semaphore(10) async def fetch(url): async with sem: # 发送请求 pass -
取消处理
长时间运行的任务应当响应用
try...except asyncio.CancelledError处理取消请求,做必要的清理工作。 -
Python 版本差异
Python 3.7+ 推荐使用
asyncio.run();3.8 引入了asyncio.to_thread();3.11 带来了 TaskGroup,更优雅地管理任务生命周期。
十、实战:简易异步爬虫骨架
python
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
pages = await asyncio.gather(*tasks, return_exceptions=True)
for url, page in zip(urls, pages):
if isinstance(page, Exception):
print(f"{url} 失败: {page}")
else:
print(f"{url} 返回长度: {len(page)}")
urls = ["https://example.com"] * 5
asyncio.run(main(urls))
总结
- 协程函数 用
async def定义,await挂起等待。 - 事件循环是调度中心,单线程并发关键。
- 并发执行 主要靠
asyncio.gather()和create_task()。 - 异步生成器 和异步推导式让你能流式处理和收集异步数据。
- 必须注意线程安全 、阻塞调用 和取消处理等细节。
掌握这些内容,你就可以在 Python 中高效地编写异步 I/O 程序了。对于 CPU 密集型任务,仍需考虑多进程或其他方案,但在 I/O 密集型场景下,协程是极轻量、易维护的并发利器。