Python 协程详解与技巧总结
一、协程是什么?
协程(Coroutine)是一种用户态的轻量级线程,由程序自主控制调度,通过协作式多任务处理实现并发。与普通函数不同,协程可以在执行中途主动挂起自己,稍后再从暂停处恢复运行。
通俗理解:普通函数像一次性的过山车,上车跑完全程到站下车;协程像一辆可以随时靠站停车、再随时启动的私家车,状态(变量和上下文)全都在。
协程 vs 线程 vs 进程
| 特性 | 协程 | 线程 | 进程 |
|---|---|---|---|
| 调度方式 | 用户态协作式(主动让出) | 内核态抢占式(时间片轮转) | 内核态独立调度 |
| 内存占用 | 1-10KB | 1-10MB | 独立地址空间(MB级) |
| 并发数量 | 10万+级 | 数百级 | 数十级 |
| 适用场景 | I/O密集型 | I/O密集型(有限并发) | CPU密集型(多核并行) |
每个协程仅需约5KB内存(线程约8MB),切换耗时仅0.1-1μs(线程切换需5-30μs)。由于所有协程在同一线程内调度,天然避免了多线程的数据竞争问题。
二、协程的演进历程
Python协程经历了三个阶段:
-
Python 2.5+(生成器协程) :通过
yield和.send()手动实现协程,语法晦涩,极易出错。 -
Python 3.4+(过渡期) :
@asyncio.coroutine+yield from,正式纳入标准库,但语法仍有"缝合感"。 -
Python 3.7+(现代协程) :
async def/await,语法优雅自然,是目前推荐的写法。
三、核心语法与API
3.1 基础语法
python
import asyncio
# async def 定义协程函数,调用返回协程对象(不立即执行)
async def say_after(delay, what):
await asyncio.sleep(delay) # await 挂起当前协程,等待异步操作完成
print(what)
# asyncio.run() 启动事件循环的入口(程序中仅调用一次)
async def main():
# 直接 await 是串行执行
await say_after(1, 'hello')
await say_after(2, 'world')
# 总耗时约 3 秒
asyncio.run(main())
-
async def:定义协程函数,返回协程对象 -
await:挂起当前协程,等待右侧可等待对象(协程/Task/Future)完成 -
asyncio.run():创建事件循环、运行协程并关闭循环,Python 3.7+推荐入口
3.2 并发执行:create_task vs gather
直接 await 是串行,要实现并发必须创建 Task:
python
async def main():
# 方式一:create_task 创建任务,实现并发
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
await task1
await task2
# 总耗时约 2 秒
# 方式二:gather 批量并发
await asyncio.gather(
say_after(1, 'hello'),
say_after(2, 'world')
)
create_task 相当于"发射后不管",后面需要结果时再 await 拿结果-。asyncio.gather 则同时等待所有任务完成。
3.3 TaskGroup(Python 3.11+)
TaskGroup 是 create_task 的更现代替代方案,上下文管理器退出时自动等待所有任务完成:
python
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(say_after(1, 'hello'))
task2 = tg.create_task(say_after(2, 'world'))
# 退出上下文时自动 await 所有任务
四、底层原理:事件循环与调度机制
4.1 事件循环(Event Loop)
事件循环是协程的"调度中心",本质上是一个无限循环,负责监听、调度和执行异步任务。
工作流程:
-
任务注册 :协程通过
create_task注册到事件循环 -
任务执行 :协程执行到
await时主动挂起,释放控制权 -
I/O监听 :事件循环通过
epoll/kqueue监听文件描述符 -
任务恢复:I/O就绪后,事件循环唤醒协程,从暂停处继续执行
4.2 协程的底层本质
async def 函数底层仍基于生成器(generator)实现:
-
遇到
await时,协程保存当前执行上下文(局部变量、指令指针),返回控制权给事件循环 -
被
await的对象就绪后,事件循环调用协程的send(None),从上次暂停处继续执行 -
整个过程在单线程内完成,没有线程切换开销
4.3 可等待对象(Awaitable)
可在 await 语句中使用的对象有三种主要类型:
-
协程(Coroutine) :
async def定义的函数 -
任务(Task) :用
create_task包装的协程,可跟踪执行状态 -
Future:表示异步操作的最终结果
五、实战技巧
技巧1:控制并发度 ------ Semaphore(信号量)
批量请求时需限制并发数,避免打爆目标服务器或耗尽本地资源:
python
import asyncio
import aiohttp
sem = asyncio.Semaphore(3) # 最多同时 3 个协程执行
async def fetch(url, session):
async with sem: # 申请并发名额,完成后自动释放
timeout = aiohttp.ClientTimeout(total=5)
async with session.get(url, timeout=timeout) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch(f"https://example.com/{i}", session) for i in range(100)]
results = await asyncio.gather(*tasks, return_exceptions=True)
建议并发数控制在50-100之间,根据目标服务器承受能力动态调整-20。
技巧2:异常处理 ------ return_exceptions
异步任务中的异常不会自动传播,使用 return_exceptions=True 收集异常作为结果返回,避免单个任务失败导致整个 gather 崩溃:
python
results = await asyncio.gather(
task1, task2, task3,
return_exceptions=True
)
for result in results:
if isinstance(result, Exception):
print(f"任务失败: {result}")
else:
print(f"任务成功: {result}")
技巧3:超时控制 ------ wait_for
python
async def long_task():
await asyncio.sleep(10)
async def main():
try:
await asyncio.wait_for(long_task(), timeout=1.0)
except asyncio.TimeoutError:
print("任务超时")
技巧4:异步上下文管理器 ------ async with
管理需要异步获取和释放的资源(数据库连接、网络连接等):
python
class AsyncDatabaseConnection:
async def __aenter__(self):
await asyncio.sleep(1) # 模拟异步连接
return self
async def __aexit__(self, exc_type, exc, tb):
await asyncio.sleep(0.5) # 模拟异步关闭
async def main():
async with AsyncDatabaseConnection() as db:
await asyncio.sleep(2) # 执行查询
技巧5:复用连接池
使用 aiohttp.ClientSession 或 httpx.AsyncClient 复用TCP连接,避免每次请求都经历三次握手:
python
async def main():
async with aiohttp.ClientSession() as session:
# 所有请求复用同一个 session,共享连接池
tasks = [fetch(url, session) for url in urls]
await asyncio.gather(*tasks)
六、常见坑与避坑指南
❌ 坑1:在协程中调用阻塞函数
python
# 错误:会冻结整个事件循环
async def bad():
time.sleep(1) # 阻塞!
requests.get(url) # 阻塞!
✅ 正确做法:使用异步替代方案
python
async def good():
await asyncio.sleep(1) # 异步休眠
async with aiohttp.ClientSession() as session:
await session.get(url) # 异步HTTP请求
❌ 坑2:混淆 await 和并发
python
# 错误:串行执行,总耗时 3 秒
await coro1()
await coro2()
await coro3()
# 正确:并发执行,总耗时约 1 秒
await asyncio.gather(coro1(), coro2(), coro3())
await 不等于并发,await asyncio.gather(...) 才是并发。
❌ 坑3:直接调用协程函数
python
# 错误:直接调用不会执行,只返回协程对象
main() # <coroutine object main at 0x...>
# 正确:必须 await 或通过 asyncio.run 执行
asyncio.run(main())
❌ 坑4:CPU密集型任务使用协程
协程提升的是I/O并发效率,不是CPU计算速度。CPU密集型任务应使用多进程或 run_in_executor,否则会阻塞事件循环:
python
import concurrent.futures
async def cpu_bound_task():
loop = asyncio.get_running_loop()
with concurrent.futures.ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(pool, heavy_computation)
return result
七、总结
| 要点 | 说明 |
|---|---|
| 适用场景 | I/O密集型任务(网络请求、文件读写、数据库查询) |
| 不适用 | CPU密集型任务(应使用多进程) |
| 核心优势 | 单线程高并发、低资源消耗、无锁竞争、代码可读性好 |
| 关键API | async def、await、asyncio.run()、create_task、gather、Semaphore |
| Python版本 | 建议 Python 3.7+,TaskGroup 需要 3.11+ |
协程的核心可以总结为一句话:在I/O等待时主动让出CPU,让单线程在等待期间去处理其他任务,从而实现高效的并发。