Python异步编程入门到实践:用asyncio写出高性能代码

在IO密集型任务(网络请求、文件读写、数据库查询)面前,Python的同步模型常常因为阻塞等待而浪费大量CPU时间。异步编程通过事件循环和协程,让单线程也能并发处理成百上千个任务,显著提升吞吐量。本文从零开始,结合代码讲解Python的asyncio库,带你写出真正的异步程序。

  1. 同步 vs 异步:一个直观对比

假设我们要模拟三个独立的IO请求,每个耗时1秒。同步版本会线性执行,总共需要3秒。异步版本可以在等待一个请求时切换到另一个,总时间接近1秒(不考虑调度开销)。

同步代码:

```python

import time

def io_task(name, seconds):

print(f'{name} 开始')

time.sleep(seconds) # 阻塞整个线程

print(f'{name} 结束')

return name

start = time.time()

results = io_task(f'task{i}', 1) for i in range(3)

print(f'总耗时: {time.time() - start:.2f}s')

```

输出:

```

task0 开始

task0 结束

task1 开始

task1 结束

task2 开始

task2 结束

总耗时: 3.00s

```

异步版本(使用asyncio):

```python

import asyncio

import time

async def io_task(name, seconds):

print(f'{name} 开始')

await asyncio.sleep(seconds) # 非阻塞等待

print(f'{name} 结束')

return name

async def main():

tasks = asyncio.create_task(io_task(f'task{i}', 1)) for i in range(3)

results = await asyncio.gather(*tasks)

print(results)

start = time.time()

asyncio.run(main())

print(f'总耗时: {time.time() - start:.2f}s')

```

输出:

```

task0 开始

task1 开始

task2 开始

task0 结束

task1 结束

task2 结束

'task0', 'task1', 'task2'

总耗时: 1.00s

```

三个任务几乎同时开始,总耗时仅1秒。关键点在于await asyncio.sleep(1)让出控制权,事件循环可以执行其他任务。

  1. 核心概念:协程、任务与事件循环

· 协程(coroutine):由async def定义的函数。调用时不会立即执行,而是返回一个协程对象。

· 任务(Task):通过asyncio.create_task()将协程包装为任务,实现"并发"调度。任务会在事件循环中独立运行。

· 事件循环(Event Loop):核心调度器,负责管理所有任务,并在它们等待IO或sleep时切换。

下图简示事件循环的工作(文字描述版):

```

事件循环启动 -> 任务A执行 -> 遇到await -> 任务A挂起,切换至任务B -> 任务B执行... -> 挂起的任务等待条件满足后恢复

```

  1. 实际案例:异步HTTP请求

结合aiohttp库(需先安装:pip install aiohttp),我们实现一个高效的并发网页抓取器。

```python

import asyncio

import aiohttp

import time

async def fetch_url(session, url):

async with session.get(url) as response:

模拟解析HTML(仅获取状态码作为示例)

status = response.status

html_len = len(await response.text())

return url, status, html_len

async def main(urls):

async with aiohttp.ClientSession() as session:

tasks = fetch_url(session, url) for url in urls

results = await asyncio.gather(*tasks, return_exceptions=True)

for result in results:

if isinstance(result, Exception):

print(f'请求失败: {result}')

else:

url, status, size = result

print(f'{url} -> 状态码 {status}, 内容长度 {size}')

if name == 'main':

test_urls = [

'https://www.python.org',

'https://www.github.com',

'https://www.stackoverflow.com',

'https://httpbin.org/delay/2', # 故意延迟2秒

]

start = time.time()

asyncio.run(main(test_urls))

print(f'总耗时: {time.time() - start:.2f}s')

```

输出示例:

```

https://www.python.org -> 状态码 200, 内容长度 50434

https://www.github.com -> 状态码 200, 内容长度 202022

https://httpbin.org/delay/2 -> 状态码 200, 内容长度 302

https://www.stackoverflow.com -> 状态码 200, 内容长度 728729

总耗时: 2.17s

```

即使存在一个延迟2秒的请求,总耗时也仅2秒出头,所有请求并发执行。

  1. 并发控制:限制同时执行的任务数

在某些场景(如API速率限制)下,我们需要控制并发数量。asyncio.Semaphore可以轻松实现。

```python

async def fetch_with_limit(sem, session, url):

async with sem: # 限制同时最多N个请求

return await fetch_url(session, url)

async def main_with_limit(urls, limit=3):

sem = asyncio.Semaphore(limit)

async with aiohttp.ClientSession() as session:

tasks = fetch_with_limit(sem, session, url) for url in urls

results = await asyncio.gather(*tasks, return_exceptions=True)

... 处理结果

```

这样最多同时发起3个请求,其余请求会等待信号量释放。

  1. 错误处理与超时

5.1 单个任务的超时

使用asyncio.wait_for为协程设置超时。

```python

try:

result = await asyncio.wait_for(coro, timeout=5.0)

except asyncio.TimeoutError:

print('任务超时')

```

5.2 整体超时

asyncio.timeout上下文管理器(Python 3.11+)或老版本的asyncio.wait_for包裹整个gather。

```python

async def main_with_timeout(urls, total_timeout=10):

try:

async with asyncio.timeout(total_timeout):

await main(urls)

except asyncio.TimeoutError:

print(f'整体执行超过{total_timeout}秒')

```

5.3 捕获任务异常

gather的return_exceptions=True会将异常作为结果返回,而不是立即抛出。或者使用asyncio.Task.exception()方法。

  1. 进阶:同步代码与异步代码混合

如果必须调用一个阻塞的第三方库(如requests),可以用asyncio.to_thread(Python 3.9+)将其放到线程池执行,避免阻塞事件循环。

```python

import requests

async def blocking_io():

在线程池中运行阻塞的requests.get

result = await asyncio.to_thread(requests.get, 'https://api.example.com/data')

return result.json()

```

对于CPU密集型任务,应当使用multiprocessing或asyncio配合concurrent.futures.ProcessPoolExecutor。

  1. 常见陷阱与最佳实践

陷阱 解决方案

在协程中调用time.sleep() 必须使用await asyncio.sleep()

忘记await协程函数 调用协程函数会返回协程对象,必须await或create_task

在同步函数中直接运行异步代码 使用asyncio.run()(但只能调用一次)或创建新事件循环

大量任务时未设置并发限制 用Semaphore限流,避免压垮服务端或超出系统限制

未处理任务异常导致静默失败 为每个任务添加错误回调或使用gather(return_exceptions=True)

  1. 总结

· 异步编程适合IO密集型任务,能成倍提升效率。

· asyncio采用协程+事件循环模型,需要你将所有阻塞操作替换为异步版本(如aiohttp替换requests)。

· 掌握create_task、gather、wait_for、Semaphore等API,可以灵活控制并发。

· 注意混合线程池/进程池处理不可避免的同步或CPU密集型代码。

现在你可以尝试将项目中的同步IO部分重写为异步模式了。异步不是银弹,但用对场景会让你的代码飞起来。

相关推荐
全栈练习生3 分钟前
大模型反向传播与梯度下降
python·ai
小小张说故事1 小时前
Python logging 日志不输出?根源在 propagate 这条链上
后端·python
喜欢打篮球的普通人1 小时前
MiniMind 学习笔记(十):优化器、学习率和数据设置——训练稳定性的三块基石
笔记·python·学习
醇氧1 小时前
uvicorn 详细介绍
linux·运维·python·python3.11
丹宇码农2 小时前
Go 与 Python 协程(Coroutine)对比演示项目
开发语言·python·golang
codists2 小时前
lexeme和token
python
天赐范式2 小时前
天赐范式第182天:让变异开始存活——主线重启与变异存续条件
python·ar模型·数字生命·天赐范式·动态运行时·稳态方差·变异存续
lusklusklusk3 小时前
Python 四大数据结构详解:元组、列表、集合、字典的特点与增删改查
数据结构·windows·python
打工仔折腾 AI3 小时前
把模型切换交给平台:用蓝耘智能路由搭建商品评论分析工具
java·服务器·前端·后端·python·性能优化·ai agent 实战
ynchyong3 小时前
python list 地常用操作
开发语言·python