Python 异步编程实战:从 asyncio 到结构化并发

发布于 2026-09-01

背景

在 IO 密集型的 Web 爬虫、批量 PDF 处理、图片压缩场景中,同步代码的性能瓶颈非常明显。本文记录我们把批量图片处理服务从同步重构为异步的完整过程,以及踩到的坑。

同步 vs 异步:什么时候该用 async

# 同步:处理 100 张图片 ≈ 20 秒
for img in images:
    compress(img)   # 每张 0.2s

# 异步(IO 密集):≈ 3 秒
async def main():
    tasks = [compress_async(img) for img in images]
    await asyncio.gather(*tasks)

关键判断标准
- CPU 密集(压缩/抠图/OCR 推理)→ 用 multiprocessing 或进程池
- IO 密集(网络请求/文件读写/API 调用)→ 用 asyncio

2024 最佳实践:结构化并发

Python 3.11 引入的 TaskGroup 让结构化并发成为可能:

async def process_batch(file_paths):
    try:
        async with asyncio.TaskGroup() as tg:
            # 批量提交任务
            tasks = [tg.create_task(process_one(p)) for p in file_paths]
            results = [task.result() for task in tasks]
            return results
    except ExceptionGroup as eg:
        # 自动捕获所有子任务异常,不会静默失败
        for exc in eg.exceptions:
            logger.error(f'任务失败: {exc}')

对比传统的 asyncio.gather(..., return_exceptions=True),TaskGroup 在异常传播、取消语义、生命周期管理上都更清晰。

我们的图片处理服务改造

阶段 方案 吞吐(张/秒) 内存占用
改造前 同步 + 进程池 4 worker 22 稳定
改造中间 asyncio + 进程池 45 略高
最终方案 asyncio + multiprocessing 68 最优

常见陷阱

陷阱 1:同步阻塞调用卡死事件循环

# ❌ 错误
async def handler():
    data = requests.get(url)   # requests 是同步的,会卡事件循环
    return data

# ✅ 正确:用 httpx 或在线程池跑同步代码
async def handler():
    data = await httpx.get(url)
    return data

陷阱 2:过度并发

一张 10MB 的 PDF 合并任务不能开 1000 个协程,磁盘 IO 会成为瓶颈。建议用 asyncio.Semaphore 做限流:

sem = asyncio.Semaphore(20)   # 最多 20 个并发文件处理

async def safe_process(path):
    async with sem:
        return await process_one(path)

总结

异步不是银弹,但在 IO 密集场景下能以极低的成本获得 5-10x 的吞吐提升。关键是:识别瓶颈在哪里、选择正确的并发模型、做合理的限流

常见问题

这篇文章的代码可以直接用吗?

文中代码示例均来自真实线上项目,但具体使用需要根据你的环境调整参数和依赖版本。

有问题想请教作者怎么办?

欢迎在文章页面留言评论,我们看到后会尽快回复。也可以通过网站联系我们。

相关工具推荐