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部分重写为异步模式了。异步不是银弹,但用对场景会让你的代码飞起来。

相关推荐
云天AI实战派1 小时前
Agent 全流程实战:用 Python 搭建技能路由智能体,落地小龙虾门店运营助手
开发语言·人工智能·python
2401_871492852 小时前
C#怎么使用泛型 C#泛型类泛型方法和泛型约束的定义和使用方法【语法】
jvm·数据库·python
我滴老baby2 小时前
工具调用全景解析从Function Calling到MCP协议的完整实践
开发语言·人工智能·python·架构·fastapi
小白学大数据2 小时前
抖音搜索页数据批量爬取,多关键词同步采集实现
爬虫·python·数据分析
2301_787312432 小时前
Vue.js中Patch过程处理Teleport组件挂载位置的特殊逻辑
jvm·数据库·python
我鑫如一2 小时前
性价比高的AI API中转站推荐企业
人工智能·python
Leinwin2 小时前
GPT-5.5 Instant API接入教程:免费额度、速率限制与最佳实践
后端·python·flask
dfdfadffa2 小时前
Golang Gin怎么做JWT登录认证_Golang Gin JWT教程【实用】
jvm·数据库·python
SilentSamsara2 小时前
装饰器基础:从闭包到装饰器的自然演变
开发语言·前端·vscode·python·青少年编程·pycharm