17、Python之异步IO

一、概述

异步IO(Asynchronous I/O)是一种单线程并发模型,适合IO密集型场景(网络请求、数据库读写、文件读取)。 核心思想:遇到IO阻塞时,不原地等待,让出CPU,先去处理其它就绪任务;等IO完成后再回来继续执行。

区分:

  • 多线程:操作系统调度,线程切换有开销;受GIL限制CPU密集收益低
  • 协程(异步):用户态调度,切换开销极小;单线程内实现并发,适合大量IO等待任务

适用场景:爬虫、大量接口请求、异步数据库;不适合CPU密集计算。


二、协程

协程(coroutine),可以理解为“可以暂停、恢复的函数”,由程序自身控制切换,不是操作系统线程。 Python 3.5+ 引入async / await语法,是协程的标准写法。

  • async def:定义协程函数,调用它不会直接执行代码,而是返回一个协程对象
  • await:后面接可等待对象(协程、Task、Future);遇到await就挂起当前协程,让出事件循环

关键概念:

  1. 事件循环 event loop:异步的调度器,负责管理、调度所有协程任务
  2. Task:对协程的封装,把协程提交给事件循环并发执行
  3. 可等待对象(Awaitable):协程、Task、Future

一句话:协程是任务本体,事件循环是调度器,Task用来并发运行多个协程。

⚠️ 注意:await只能写在async def函数内部,普通同步函数不能用await。


三、使用 asyncio

asyncio是Python内置异步IO标准库,提供事件循环、Task、同步原语等。

1. 基础单协程示例

import asyncio async def hello(): print("hello start") # 异步睡眠,让出循环,不会阻塞整个线程 await asyncio.sleep(1) print("hello end") async def main(): await hello() if __name__ == "__main__": asyncio.run(main())

asyncio.run():自动创建事件循环、运行协程、关闭循环(Python3.7+推荐写法)

2. 并发执行多个协程(asyncio.gather)

import asyncio async def say(name, delay): print(f"{name} 开始,等待 {delay}s") await asyncio.sleep(delay) print(f"{name} 结束") async def main(): # gather 收集多个协程,并发执行,等待全部完成 task1 = say("A", 2) task2 = say("B", 1) await asyncio.gather(task1, task2) if __name__ == "__main__": asyncio.run(main())

运行总耗时 ≈ 2s,不是 3s,证明是并发。

3. 手动创建 Task

import asyncio async def work(n): await asyncio.sleep(n) print(f"task {n} done") async def main(): # 创建Task,立刻提交到事件循环调度 t1 = asyncio.create_task(work(1)) t2 = asyncio.create_task(work(2)) await t1 await t2 if __name__ == "__main__": asyncio.run(main())

4. 异步与同步代码坑点

  • 同步阻塞函数(time.sleep()、同步MySQL、requests)会卡死事件循环!
  • 解决方案:改用异步库(aiohttp、aiomysql);或者asyncio.to_thread()放到子线程运行同步代码
import asyncio import time def sync_block(): time.sleep(1) return "同步结果" async def main(): res = await asyncio.to_thread(sync_block) print(res) asyncio.run(main())

四、aiohttp

aiohttp是基于asyncio的异步HTTP客户端/服务端框架,替代同步的requests,适合并发爬取网页、批量调用接口。

安装

pip install aiohttp

1. 基础GET请求

import asyncio import aiohttp async def fetch(url): # 创建会话,类比 requests.Session async with aiohttp.ClientSession() as session: async with session.get(url) as resp: print("状态码:", resp.status) # 读取文本 text = await resp.text() return text[:100] async def main(): url = "[https://httpbin.org/get](https://httpbin.org/get)" result = await fetch(url) print(result) if __name__ == "__main__": asyncio.run(main())

2. 批量并发请求

import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as resp: return await resp.text() async def main(): urls = [ "[https://httpbin.org/get](https://httpbin.org/get)", "[https://httpbin.org/headers](https://httpbin.org/headers)", "[https://httpbin.org/ip](https://httpbin.org/ip)" ] async with aiohttp.ClientSession() as session: tasks = [fetch(session, u) for u in urls] results = await asyncio.gather(*tasks) for r in results: print(r[:80]) if __name__ == "__main__": asyncio.run(main())

最佳实践:复用同一个 ClientSession,不要每次请求新建Session,减少连接开销。

3. POST提交表单

async def post_demo(session): data = {"username": "test", "pwd": "123456"} async with session.post("[https://httpbin.org/post](https://httpbin.org/post)", data=data) as resp: return await resp.json()

4. 常用配置

  • 超时:timeout=aiohttp.ClientTimeout(total=10)
  • 并发限制:搭配asyncio.Semaphore控制并发数量,防止请求打爆服务
sem = asyncio.Semaphore(3) # 最多同时3个请求 async def fetch_limit(session, url): async with sem: return await fetch(session, url)

五、对比总结

概念作用
协程可暂停恢复的异步函数,async def定义
asyncio内置异步调度库,提供事件循环、gather、Task
aiohttp异步HTTP库,异步爬虫、接口调用首选

六、常见坑

  1. 混用requests + asyncio:requests是同步阻塞,会卡住整个事件循环
  2. 忘记 await:协程对象不await,代码不会执行,也不会报错
  3. 无限制并发请求,触发对方服务器限流、封IP → 使用Semaphore限流
  4. 不要在协程里面写耗时CPU计算,异步不擅长这个