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

相关推荐
weixin_BYSJ19876 小时前
SpringBoot + MySQL 乒乓球运动员信息管理系统项目实战--附源码04954
java·javascript·spring boot·python·django·flask·php
EQUINOX17 小时前
【论文阅读】| MoCo精读
论文阅读·人工智能·python·深度学习·机器学习
用户8356290780518 小时前
使用 Python 自动化 Excel 公式和函数:完整指南
后端·python
敲代码的嘎仔8 小时前
实习日志day6--实习日志day6--title命名规范化&businessType纠正&补充缺失的@Log注解&报警与通信模块补充&产出阶段总结文档
java·开发语言·人工智能·git·python·实习·大二
江华森8 小时前
Python 实现高德地图找房(一):环境搭建与数据爬虫
开发语言·爬虫·python
VIP_CQCRE9 小时前
用 Ace Data Cloud 快速接入 OpenAI Chat Completions:对话、流式、多轮和多模态一篇打通
python·ai·openai·api·教程
AOwhisky9 小时前
Python 学习笔记(第三期)——流程控制:条件判断与循环结构
运维·笔记·python·学习·云原生·流程控制·循环
老迟到的茉莉10 小时前
Hermes 是谁?跟 Claude Code 差在哪
开发语言·python
Day(AKA Elin)10 小时前
【Day】MTP(Multi Token Prediction)技术学习
python·深度学习·学习·llama
DogDaoDao11 小时前
【GitHub】 LLVM Project 深度解析:现代编译器基础设施的基石
java·c++·python·程序员·github·编译器·llvm