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

相关推荐
笨鸟先飞,勤能补拙1 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能2 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂2 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study2 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境3 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境3 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd1233 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy12393102164 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb004 小时前
一个项目带你入门AI应用开发01
python
0566465 小时前
Python康复训练——常用标准库
开发语言·python·学习