Python脚本如何批量执行异步任务

wen python案例 24

Python脚本如何批量执行异步任务:从入门到高效实战

目录导读

  1. 为什么需要批量异步任务?
  2. 核心工具:asyncio与aiohttp/aiomysql
  3. 三步搭建异步任务框架
  4. 常见陷阱与性能调优
  5. FAQ:新手最常问的5个问题
  6. 实战案例:批量抓取1000个网页

为什么需要批量异步任务?

当业务需要处理大量I/O密集型操作(如网络请求、数据库读写、文件下载)时,传统的同步方式会因等待I/O而浪费CPU资源,用requests库抓取100个网页,同步代码需要按顺序等待每个请求完成,总耗时≈单个请求耗时×100,而异步任务允许在等待某个请求时切换执行其他任务,理论上总耗时≈最慢的单个请求耗时。

Python脚本如何批量执行异步任务

适用场景:

  • 爬虫批量采集数据
  • 批量发送邮件/短信
  • 微服务间批量API调用
  • 数据库批量读写(需配合连接池)

不适用场景:

  • CPU密集型计算(如图像渲染、数据分析)——此时应使用multiprocessing

核心工具:asyncio与aiohttp/aiomysql

1 Python原生异步库:asyncio

asyncio是Python 3.4+内置的异步I/O框架,通过async/await语法定义协程,核心概念:

  • 事件循环:调度协程的核心,负责监听I/O事件并回调。
  • 协程:通过async def定义的函数,需用await挂起。
  • 任务:通过asyncio.create_task()将协程包装为可并发执行的任务。
import asyncio
async def fetch(url):
    # 模拟异步请求
    await asyncio.sleep(1)
    return f"Fetched {url}"
async def main():
    tasks = [asyncio.create_task(fetch(f"page_{i}")) for i in range(10)]
    results = await asyncio.gather(*tasks)
    print(results)
asyncio.run(main())

2 第三方异步库:aiohttp(HTTP)、aiomysql(数据库)

  • aiohttp:异步HTTP客户端/服务器库,支持Session复用、超时控制。
  • aiomysql:异步MySQL驱动,需配合asyncio和连接池使用。

安装:

pip install aiohttp aiomysql

三步搭建异步任务框架

步骤1:定义异步任务函数

async def async_task(item):
    async with aiohttp.ClientSession() as session:
        async with session.get(f"https://api.example.com/{item}") as resp:
            return await resp.json()

步骤2:批量创建任务并调度

async def main():
    items = range(1000)  # 假设有1000个任务
    tasks = [asyncio.create_task(async_task(i)) for i in items]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    # return_exceptions=True会捕获异常而不中断

步骤3:限流与资源控制

使用asyncio.Semaphore控制并发数,防止资源耗尽:

sem = asyncio.Semaphore(50)  # 最多50个并发
async def async_task_with_limit(item):
    async with sem:
        return await async_task(item)

常见陷阱与性能调优

1 陷阱:阻塞任务混入

在异步协程中调用time.sleep()或同步库(如requests)会阻塞事件循环,导致所有任务等待。
解决方案: 使用asyncio.sleep()asyncio.to_thread()包装同步代码。

2 陷阱:连接数超限

批量请求时,ClientSession默认连接数为100,超过会排队。
调优: session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=500))

3 陷阱:内存泄漏

未正确关闭Session或文件句柄会导致内存占用持续增长。
方案: 使用async with自动释放资源,或显式调用session.close()

4 性能调优要点

  • 复用Session:每个任务创建Session是低效的,应在主函数中创建全局Session。
  • 控制并发数:根据目标服务器能力调整Semaphore值,通常50-200之间。
  • 超时设置:为每个请求设置超时,避免死等:session.get(url, timeout=10)

FAQ:新手最常问的5个问题

Q1:异步编程能否用多线程替代?
A:多线程适合I/O不密集或需要共享状态的场景,但Python的GIL会使多线程在CPU密集型任务中反而更慢,异步是单线程协作式调度,更轻量。

Q2:异步任务中如何打印日志?
A:使用logging模块时,注意日志写入文件是I/O操作,建议用logging.handlers.QueueHandler异步写入,或使用aiologger库。

Q3:失败任务如何重试?
A:在async_task内用循环实现重试,配合tenacity库简化:

from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=5))
async def async_task_retry(item):
    # ...

Q4:为何我的异步代码比同步还慢?
A:检查是否混入了同步阻塞操作,或并发数过低。asyncio适用于I/O密集型,若任务本身计算量大(如正则匹配),应考虑concurrent.futures

Q5:异步如何与数据库交互?
A:使用aiomysqlasyncpg,避免用pymysql,对于ORM,选择sqlalchemy的异步模式(1.4+版本支持)。


实战案例:批量抓取1000个网页

目标:抓取https://httpbin.org/delay/1(模拟延迟1秒的页面)1000次,并统计总耗时。

import asyncio
import aiohttp
import time
async def fetch_page(session, url):
    async with session.get(url, timeout=10) as resp:
        return await resp.text()
async def main():
    urls = [f"https://httpbin.org/delay/1?n={i}" for i in range(1000)]
    conn = aiohttp.TCPConnector(limit=100)
    sem = asyncio.Semaphore(80)
    async with aiohttp.ClientSession(connector=conn) as session:
        async def limited_fetch(url):
            async with sem:
                return await fetch_page(session, url)
        tasks = [asyncio.create_task(limited_fetch(url)) for url in urls]
        start = time.time()
        results = await asyncio.gather(*tasks, return_exceptions=True)
        end = time.time()
        successes = [r for r in results if not isinstance(r, Exception)]
        print(f"成功: {len(successes)}, 失败: {len(results)-len(successes)}")
        print(f"总耗时: {end-start:.2f}秒")
asyncio.run(main())

执行效果:

  • 若同步执行(单线程串行),1000个请求各延迟1秒,总耗时≈1000秒。
  • 异步并发80个任务,总耗时约12-15秒(网络延迟+服务器处理时间)。

优化空间:

  • 预解析URL,减少字符串拼接。
  • 使用asyncio.as_completed及时处理已完成的响应。

批量异步任务的核心是用协程消除I/O等待,通过asyncio+第三方异步库实现高效并发,关键在于:明确任务类型、控制并发数、避免阻塞、妥善处理异常,掌握本文的技术点后,您可以将脚本轻松改造,应对每日百万级API调用或数据库批量写入场景。

抱歉,评论功能暂时关闭!