在传统同步编程中,一个耗时的 I/O 操作(如网络请求、文件读取)会阻塞整个线程,导致 CPU 空闲等待。例如,用 requests
同步请求 100 个网页,需要依次等待每个请求完成,总耗时可能超过 30 秒;而用异步编程(aiohttp
+ asyncio
),这些请求可以“并行”发起,总耗时仅需 2-3 秒。
Python 的异步编程以 asyncio
库为核心,通过**协程(Coroutine)和事件循环(Event Loop)**实现高效的 I/O 并发。本文将从异步 I/O 的底层原理出发,结合网络请求、文件读写等实战案例,带你掌握异步编程的核心技巧,彻底解决高并发场景下的性能瓶颈。
异步 I/O 的核心是:当程序发起一个 I/O 请求(如读取文件、发送 HTTP 请求)时,CPU 不会阻塞等待结果,而是去执行其他任务;当 I/O 操作完成(数据就绪)时,程序再回来处理结果。
对比同步 I/O 和异步 I/O 的执行流程:
协程(Coroutine,简称 coro
)是异步编程的“执行单元”,本质是可以暂停和恢复的函数。与线程(内核态)相比,协程的优势:
事件循环是异步程序的核心调度器,负责:
简单来说,事件循环就像一个“任务队列”,不断检查哪些任务的 I/O 操作已完成,并唤醒对应的协程继续执行。
在 Python 中,协程函数通过 async def
定义,用 await
关键字暂停执行并等待异步操作完成。
示例:简单的协程函数
import asyncio
async def hello(name: str, delay: float):
print(f"[{asyncio.get_event_loop().time():.2f}] 开始执行 {name}")
await asyncio.sleep(delay) # 模拟异步 I/O(非阻塞)
print(f"[{asyncio.get_event_loop().time():.2f}] {name} 完成")
# 运行协程(需通过事件循环)
asyncio.run(hello("任务A", 1.0)) # 输出:开始执行任务A → 1秒后完成任务A
示例:并发运行多个协程
async def main():
# 创建任务(自动加入事件循环)
task1 = asyncio.create_task(hello("任务A", 1.0))
task2 = asyncio.create_task(hello("任务B", 0.5))
# 等待所有任务完成
await task1
await task2
asyncio.run(main())
输出结果(时间戳为相对值):
[0.00] 开始执行 任务A
[0.00] 开始执行 任务B
[0.50] 任务B 完成
[1.00] 任务A 完成
await
是异步编程的核心语法,作用是:
await
后的异步操作完成时,恢复当前协程的执行。注意:await
后必须是一个“可等待对象”(如协程、Future
、Task
),否则会抛出 TypeError
。
aiohttp
是 Python 中最常用的异步 HTTP 库,支持异步的 HTTP 请求和 Web 服务。以下是用 aiohttp
并发请求 10 个 URL 的示例:
pip install aiohttp
import asyncio
import aiohttp
async def fetch_url(session: aiohttp.ClientSession, url: str):
async with session.get(url) as response:
content = await response.text() # 异步读取响应内容
return f"{url}: 响应长度 {len(content)}"
async def main(urls: list):
async with aiohttp.ClientSession() as session: # 复用连接池
tasks = [asyncio.create_task(fetch_url(session, url)) for url in urls]
results = await asyncio.gather(*tasks) # 等待所有任务完成
for result in results:
print(result)
if __name__ == "__main__":
urls = [f"https://httpbin.org/get?num={i}" for i in range(10)]
asyncio.run(main(urls))
requests
):10 个请求耗时约 5-8 秒(依次等待);aiohttp
):10 个请求耗时约 0.5-1 秒(并发执行)。Python 内置的 open()
是同步的,异步文件读写需使用 aiofiles
库(基于 asyncio
和 threadpool
实现)。
pip install aiofiles
import asyncio
import aiofiles
async def read_large_file(file_path: str):
async with aiofiles.open(file_path, mode='r') as f:
content = await f.read() # 异步读取整个文件
return len(content)
async def main():
files = [f"data_{i}.txt" for i in range(5)] # 5个大文件(各1GB)
tasks = [asyncio.create_task(read_large_file(file)) for file in files]
results = await asyncio.gather(*tasks)
print(f"总读取字节数:{sum(results)}")
asyncio.run(main())
aiofiles
通过线程池将同步 I/O 转换为异步操作(避免阻塞事件循环);在实际场景中,可能需要动态创建任务(如处理实时数据流)或取消超时任务(如避免长时间等待)。
示例:带超时的任务执行
async def long_running_task():
await asyncio.sleep(10) # 模拟耗时操作
return "任务完成"
async def main():
try:
# 等待任务 3 秒,超时则取消
result = await asyncio.wait_for(long_running_task(), timeout=3.0)
print(result)
except asyncio.TimeoutError:
print("任务超时,已取消")
asyncio.run(main()) # 输出:任务超时,已取消
asyncio.Queue
是异步版的线程队列,用于在协程间安全传递数据(如爬虫中的 URL 分发)。
示例:异步生产者-消费者
import asyncio
async def producer(queue: asyncio.Queue):
for i in range(5):
await queue.put(i) # 生产数据
print(f"生产者:放入数据 {i}")
await asyncio.sleep(0.5) # 模拟生产耗时
async def consumer(queue: asyncio.Queue):
while True:
data = await queue.get() # 消费数据(阻塞直到有数据)
print(f"消费者:取出数据 {data}")
queue.task_done() # 标记任务完成
if data == 4: # 结束条件
break
async def main():
queue = asyncio.Queue(maxsize=3) # 队列最大容量3
producer_task = asyncio.create_task(producer(queue))
consumer_task = asyncio.create_task(consumer(queue))
await producer_task
await consumer_task
await queue.join() # 等待所有任务完成
asyncio.run(main())
异步编程的性能瓶颈通常来自同步代码的阻塞。以下是常见误区与优化方法:
误区 | 优化方法 |
---|---|
使用 time.sleep() |
替换为 asyncio.sleep() (非阻塞) |
调用同步 I/O 函数(如 open().read() ) |
使用 aiofiles 等异步库 |
执行 CPU 密集型操作 | 将计算任务放入线程池(loop.run_in_executor ) |
示例:将 CPU 密集型任务放入线程池
import asyncio
from concurrent.futures import ThreadPoolExecutor
def cpu_intensive_task(n: int) -> int:
# 模拟计算斐波那契数列(CPU 密集)
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
async def main():
loop = asyncio.get_event_loop()
executor = ThreadPoolExecutor(max_workers=2) # 创建线程池
# 将 CPU 任务提交到线程池(避免阻塞事件循环)
result = await loop.run_in_executor(executor, cpu_intensive_task, 100000)
print(f"斐波那契结果:{result}")
asyncio.run(main())
在异步函数中使用同步 I/O:
错误:在 async def
函数中调用 requests.get()
(同步阻塞),导致事件循环卡住;
正确:使用 aiohttp
等异步库,或通过 loop.run_in_executor
将同步 I/O 放入线程池。
忘记 await
关键字:
错误:直接调用协程函数(如 hello("任务A", 1.0)
)但未 await
,导致协程不会执行;
正确:必须通过 await
、asyncio.create_task()
或 asyncio.gather()
触发协程执行。
事件循环未正确关闭:
错误:手动管理事件循环时(如 loop = asyncio.get_event_loop()
),未调用 loop.close()
,导致资源泄漏;
正确:优先使用 asyncio.run()
(自动关闭循环),或在手动模式中确保 loop.close()
被调用。
过度并发导致资源耗尽:
错误:同时创建 10 万个任务(如爬取 10 万 URL),导致内存溢出;
正确:使用 asyncio.Semaphore
限制并发数(如 sem = asyncio.Semaphore(100)
,每次最多 100 个任务)。
异常处理不规范:
错误:未捕获协程中的异常,导致整个事件循环崩溃;
正确:使用 try...except
包裹 await
语句,或通过 asyncio.gather()
的 return_exceptions=True
参数收集异常。
通过本文的学习,你已掌握:
asyncio
库的基础使用(协程、事件循环、任务管理);aiohttp
)和文件读写(aiofiles
)的异步实现;异步编程是 Python 处理高并发 I/O 场景的“杀手锏”,但需注意:它更适合 I/O 密集型任务(如网络爬虫、API 服务),而 CPU 密集型任务(如图像处理、大数据计算)仍需结合多进程(multiprocessing
)或分布式计算(如 Dask)。
下一次遇到“请求耗时过长”“并发量上不去”的问题时,不妨试试异步编程——用协程让你的程序“同时”处理成百上千个任务,彻底释放 Python 的并发潜力!