协程
协程(Coroutine):又称微线程,是一种用户态的轻量级“任务”。
- 线程由操作系统内核调度;协程由程序员/语言运行时调度,切换无需陷入内核,开销极小。
- 同一线程内运行多个协程,同一时刻只有一个协程执行。
- 核心特性:可暂停、可恢复,函数执行到某处主动让出控制权,后续可以从暂停点继续运行。
进程 → 线程 → 协程 层级关系:
一个进程包含多个线程;一个线程内可以运行多个协程。
适用场景
✅ IO密集型任务(网络请求、文件读写、数据库访问、等待sleep)
❌ CPU密集型任务(如科学计算,协程无法利用多核,需要多进程)
原理:IO操作时线程会阻塞等待,协程可以在等待期间切换执行其他任务,充分利用等待时间。
Python协程
发展
- Python2.5+:利用生成器
yield手工模拟协程,无官方调度框架; - Python3.4:新增标准库
asyncio,实现标准化的生成器协程异步方案; - Python3.5:引入
async/await语法,提供原生协程;asyncio升级兼容原生协程,成为现代异步开发主流。
协程、线程、进程横向对比
| 类型 | 调度者 | 切换开销 | 是否多核 | 适合场景 |
|---|---|---|---|---|
| 进程 | OS内核 | 极大 | ✅ 多核 | CPU密集、隔离性要求高 |
| 线程 | OS内核 | 中等 | ✅(GIL限制python) | IO密集,老旧同步代码 |
| 协程 | 用户程序 | 极小 | ❌单线程 | IO密集、高并发网络(爬虫、接口服务) |
Python GIL限制:同一时刻一个进程内只有1个线程执行Python字节码;所以多线程无法加速CPU计算,仅适合IO等待。
协程语法(async/await)
1. async 定义协程函数
import asyncio
# 协程函数
async def demo():
print("开始执行")
await asyncio.sleep(1) # 让出控制权,暂停当前协程
print("结束执行")
# 调用协程函数!不会运行代码,只会创建【协程对象】
coro = demo()
print(type(coro)) # <class 'coroutine'>
关键点:
async def func() → 协程函数func() → 协程对象,只是一个待执行任务,不会立刻运行。
2. await 关键字
await xxx
约束:
await只能写在async def函数内部await后面必须是可等待对象(Awaitable)- 协程对象(coroutine)
- Task
- Future
作用: 执行到 await 时,暂停当前协程,把事件循环使用权交出去;等待目标可等待对象完成后,恢复当前协程继续执行。
❌ 禁止
禁止 await time.sleep(1),time.sleep() 是同步阻塞!会卡住整个线程,要用异步版本 asyncio.sleep()
3. 运行协程
import asyncio
async def demo():
print("任务启动")
await asyncio.sleep(1)
print("任务完成")
# 启动事件循环,运行协程,结束后自动关闭循环
asyncio.run(demo())
asyncio.run():创建事件循环 → 运行协程 → 关闭循环,主入口首选。
并发执行多个协程
直接顺序await是串行执行,无法并发!
# ❌ 串行,总耗时≈2秒
async def main():
await demo1()
await demo2()
想要并发,必须使用下面两种方法:
- 创建任务
asyncio.create_task() - 使用
asyncio.gather()(最常用)
方案1:asyncio.gather() 批量并发(最常用)
接收多个协程,统一调度,收集所有返回结果
import asyncio
async def work(name, delay):
print(f"{name} 开始,等待{delay}s")
await asyncio.sleep(delay)
print(f"{name} 完成")
return f"{name}结果"
async def main():
# 并发执行,总耗时 = 最长等待时间 2s
results = await asyncio.gather(
work("任务A", 1),
work("任务B", 2),
work("任务C", 1.5)
)
print("所有结果:", results)
asyncio.run(main())
方案2:asyncio.create_task() 创建Task
Task:将协程包装成调度任务,立刻在事件循环中后台运行
async def main():
# 创建任务,立即调度执行
t1 = asyncio.create_task(work("A", 1))
t2 = asyncio.create_task(work("B", 2))
# 等待任务结束,获取返回值
res1 = await t1
res2 = await t2
print(res1, res2)
适用场景:需要单独控制每个任务、取消任务、获取状态。
gather 底层本质也是批量创建Task。
区别小结
gather:适合一批任务统一执行,一次性收集结果;create_task:灵活,可以单独await、单独cancel、中途获取任务状态。
三大可等待对象 Awaitable
- Coroutine 协程对象:async函数调用得到
- Task:协程的包装,用于并发调度
- Future:底层低级接口,表示未来完成操作的结果,日常开发极少直接使用
简单理解:
- Future 是底层占位符
- Task 是 Future 的子类,专门用来运行协程
协程关键操作
1. 任务取消 cancel()
async def main():
task = asyncio.create_task(work("A", 5))
await asyncio.sleep(1)
task.cancel() # 取消任务
try:
await task
except asyncio.CancelledError:
print("任务被取消")
2. 设置超时 asyncio.wait_for()
async def main():
# 最多等待2秒,超时抛出TimeoutError
try:
await asyncio.wait_for(work("A", 3), timeout=2)
except asyncio.TimeoutError:
print("任务超时")
3. asyncio.wait() 等待一组任务
和gather不同,返回 (已完成集合,未完成集合),适合先等待任意任务完成:
tasks = [
asyncio.create_task(work("A",1)),
asyncio.create_task(work("B",2))
]
done, pending = await asyncio.wait(tasks)
协程原理:事件循环 EventLoop
事件循环是协程的调度器,整个异步程序的心脏。
执行流程:
- 保存所有待执行任务
- 循环检查:
- 就绪任务 → 执行,直到遇到await主动暂停
- IO阻塞任务 → 挂起,等待IO就绪
- 不断轮换执行就绪协程
- 所有任务完成,循环退出
⚠️ 单线程!同一时间只能执行一段代码
如果协程内部写耗时同步代码(time.sleep()、同步 requests 请求),整个事件循环 卡死,所有协程全部暂停!
同步阻塞代码解决方案
方式1:使用对应的异步库
aiohttp(代替requests)、aiomysql(代替pymysql)
方式2:同步代码放到线程池执行
线程池:asyncio.to_thread()(Python3.9+)
import time
async def main():
# 在独立线程运行同步阻塞函数,不卡住事件循环
result = await asyncio.to_thread(time.sleep, 2)
常见坑(高频易错点)
坑1:忘记await,直接丢协程对象
async def main():
work("A",1) # ❌ 只创建协程对象,没有调度,代码永远不执行!
坑2:混用同步阻塞接口
import time
async def bad():
time.sleep(1) # ❌ 全局阻塞,所有协程卡住
坑3:误以为协程能利用多核CPU
协程是单线程调度。CPU密集场景必须用 multiprocessing 多进程。
坑4:在协程中使用普通for循环大量同步操作,没有切出
长时间无await,事件循环无法切换其他任务,造成“饥饿”。
补充
生产环境可使用 uvloop,替换默认事件循环,大幅提升性能(linux/mac)