python 协程(asyncio、async、await)

协程

协程(Coroutine):又称微线程,是一种用户态的轻量级“任务”。

  • 线程由操作系统内核调度;协程由程序员/语言运行时调度,切换无需陷入内核,开销极小。
  • 同一线程内运行多个协程,同一时刻只有一个协程执行。
  • 核心特性:可暂停、可恢复,函数执行到某处主动让出控制权,后续可以从暂停点继续运行。

进程 → 线程 → 协程 层级关系:
一个进程包含多个线程;一个线程内可以运行多个协程。

适用场景

✅ IO密集型任务(网络请求、文件读写、数据库访问、等待sleep)
❌ CPU密集型任务(如科学计算,协程无法利用多核,需要多进程)

原理:IO操作时线程会阻塞等待,协程可以在等待期间切换执行其他任务,充分利用等待时间。

Python协程

发展

  1. Python2.5+:利用生成器 yield 手工模拟协程,无官方调度框架;
  2. Python3.4:新增标准库 asyncio,实现标准化的生成器协程异步方案;
  3. 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
约束:

  1. await 只能写在 async def 函数内部
  2. 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

  1. Coroutine 协程对象:async函数调用得到
  2. Task:协程的包装,用于并发调度
  3. 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

事件循环是协程的调度器,整个异步程序的心脏。

执行流程:

  1. 保存所有待执行任务
  2. 循环检查:
    • 就绪任务 → 执行,直到遇到await主动暂停
    • IO阻塞任务 → 挂起,等待IO就绪
  3. 不断轮换执行就绪协程
  4. 所有任务完成,循环退出

⚠️ 单线程!同一时间只能执行一段代码
如果协程内部写耗时同步代码(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)


原文出处:/show_1GW3mBjbwcbw.html