Python 协程详解与技巧总结

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+)

TaskGroupcreate_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)

事件循环是协程的"调度中心",本质上是一个无限循环,负责监听、调度和执行异步任务。

工作流程

  1. 任务注册 :协程通过 create_task 注册到事件循环

  2. 任务执行 :协程执行到 await 时主动挂起,释放控制权

  3. I/O监听 :事件循环通过 epoll/kqueue 监听文件描述符

  4. 任务恢复: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.ClientSessionhttpx.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 defawaitasyncio.run()create_taskgatherSemaphore
Python版本 建议 Python 3.7+,TaskGroup 需要 3.11+

协程的核心可以总结为一句话:在I/O等待时主动让出CPU,让单线程在等待期间去处理其他任务,从而实现高效的并发。

相关推荐
极光代码工作室2 小时前
基于YOLO目标检测的智能监控系统
python·深度学习·yolo·机器学习·计算机视觉
江华森2 小时前
Python 进阶编程实战 — 从多版本环境到百万级登录系统
python
C+-C资深大佬2 小时前
python while循环
服务器·开发语言·python
zh路西法3 小时前
【现代控制理论与卡尔曼滤波】从状态空间到Python仿真实现
开发语言·python
Vodka~4 小时前
WSL2 + RViz GPU渲染机械臂
人工智能·python
8Qi84 小时前
hello-agents学习笔记--Memory让Agent拥有记忆
人工智能·python·llm·agent·ai编程·vibecoding
Esaka_Forever5 小时前
Python 完整内存管理机制详解
开发语言·python·spring
Weigang5 小时前
用 LlamaIndex 做 RAG 前,先把 Reader、Index、Retriever 的边界写清楚
人工智能·python·开源