生成一个移动端标题,只需要一个标题,不需要其他的任何文本。

0 阅读3分钟

Python 异步编程实战:用 asyncio 把接口耗时从 30 秒压到 3 秒

一、问题场景

假设要批量拉取 30 个外部接口的数据做聚合,同步写法如下:

import requests, time

def fetch(url):
    return requests.get(url, timeout=5).json()

start = time.time()
data = [fetch(u) for u in URLS]   # 30 个串行请求
print(time.time() - start)        # 约 30s

这类任务的瓶颈在 I/O 等待,CPU 基本空转。线程池能缓解,但线程切换与内存开销随并发数线性增长;asyncio 用单线程协程在等待期间切换任务,更适合高并发 I/O 场景。

二、核心机制

  • 协程(coroutine):async def 定义,调用后返回协程对象,不会立即执行。
  • 事件循环(event loop):调度所有就绪任务,是异步运行的中枢。
  • await:把控制权交还事件循环,遇到 I/O 时才真正让出,因此必须使用异步版库。
  • 并发原语:asyncio.gather 批量并发;Semaphore 限流;asyncio.timeout 控制总耗时。

一个关键原则:不要在协程里写阻塞调用。requests、time.sleep、同步数据库驱动都会阻塞整个事件循环,把并发全部拖死。

三、代码演示

import asyncio, aiohttp, time

URLS = [f"[https://httpbin.org/delay/1?n={i}](https://httpbin.org/delay/1?n={i})" for i in range(30)]
CONCURRENCY = 10          # 单域名并发上限,避免压垮对端

async def fetch(session, url, sem):
    async with sem:                       # 限流
        async with session.get(url, timeout=5) as resp:
            return await resp.json()

async def main():
    sem = asyncio.Semaphore(CONCURRENCY)
    timeout = aiohttp.ClientTimeout(total=5)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        tasks = [fetch(session, u, sem) for u in URLS]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    ok = [r for r in results if not isinstance(r, Exception)]
    print(f"成功 {len(ok)} / {len(URLS)}")
    return ok

if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    print(f"总耗时 {time.time() - start:.2f}s")   # 约 3.2s

耗时从 30s 降到 3s 左右,本质是把「串行等待」改成了「并行等待」。

再加一层超时兜底与重试,生产环境更稳:

async def fetch_with_retry(session, url, sem, retry=2):
    for attempt in range(retry + 1):
        try:
            async with sem:
                async with asyncio.timeout(5):
                    async with session.get(url) as resp:
                        resp.raise_for_status()
                        return await resp.json()
        except (asyncio.TimeoutError, aiohttp.ClientError):
            if attempt == retry:
                raise
            await asyncio.sleep(2 ** attempt)   # 指数退避

四、避坑清单

  1. 同步库混用:非要用 requests 时,用 asyncio.to_thread() 包一层,别直接调用。
  2. 无上限并发:gather 一次性展开三千个任务会打爆连接池,必须加 Semaphore。
  3. 异常吞没:gather 默认遇到异常即抛出,其余任务被取消;需要全部结果时加 return_exceptions=True。
  4. CPU 密集任务:异步无收益,应交给 ProcessPoolExecutor。
  5. 资源未释放:ClientSession 必须用 async with 管理,否则连接泄漏。

五、小结

asyncio 的收益来自 I/O 等待占比。判断标准很直接:任务耗时是否主要花在等网络、等磁盘、等数据库。如果是,异步能把等待时间重叠起来;如果主要花在计算,异步只会增加复杂度。落地时先量化单次请求耗时与并发上限,再决定协程数量,比盲目堆并发更可靠。

海量精选技术文档和实战案例持续更新,敬请关注【风骏时光少年】