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就挂起当前协程,让出事件循环
关键概念:
- 事件循环 event loop:异步的调度器,负责管理、调度所有协程任务
- Task:对协程的封装,把协程提交给事件循环并发执行
- 可等待对象(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 aiohttp1. 基础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库,异步爬虫、接口调用首选 |
六、常见坑
- 混用
requests + asyncio:requests是同步阻塞,会卡住整个事件循环 - 忘记 await:协程对象不await,代码不会执行,也不会报错
- 无限制并发请求,触发对方服务器限流、封IP → 使用Semaphore限流
- 不要在协程里面写耗时CPU计算,异步不擅长这个