1. 为什么需要异步编程?
在传统的同步编程模型中,代码按照顺序逐行执行,当遇到I/O操作(如网络请求、文件读写)时,整个程序会阻塞等待操作完成。这种模式在处理高并发场景时效率低下,因为CPU大部分时间都在等待I/O而不是执行实际计算。
举个例子,假设我们要爬取100个网页:
python复制import requests
def fetch(url):
return requests.get(url).text
urls = ['http://example.com/1', 'http://example.com/2', ...] # 100个URL
for url in urls:
content = fetch(url) # 每次请求都会阻塞程序
process(content)
这种同步方式下,总耗时是所有请求时间的总和。而异步编程可以让我们在等待一个请求响应时去处理其他请求,总耗时接近最慢的那个请求的时间。
注意:异步编程不是万能的,它最适合I/O密集型场景。对于CPU密集型任务,多进程通常是更好的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Asyncio核心概念解析
2.1 事件循环(Event Loop)
事件循环是asyncio的核心,它负责调度和执行协程。你可以把它想象成一个无限循环,不断检查哪些协程可以继续执行,哪些需要等待I/O。
创建和运行事件循环的基本方式:
python复制import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
asyncio.run(main()) # Python 3.7+推荐方式
2.2 协程(Coroutine)
协程是asyncio的基本执行单元,通过async def定义。与普通函数不同,协程不会立即执行,而是返回一个协程对象,需要被事件循环调度。
关键区别:
- 普通函数:
def func(): return 1 - 协程函数:
async def coro(): return 1
2.3 Future和Task
- Future:表示一个尚未完成的计算结果
- Task:是Future的子类,用于包装和管理协程的执行
创建任务的两种方式:
python复制# 方式1
task = asyncio.create_task(coro())
# 方式2(不推荐,适用于旧版本)
task = asyncio.ensure_future(coro())
3. 实战:构建异步网络爬虫
让我们用asyncio实现一个高效的网页爬虫。我们将使用aiohttp库(异步HTTP客户端)和async with语法。
3.1 基础爬虫实现
python复制import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://example.org',
'https://example.net'
]
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for url, content in zip(urls, results):
print(f"{url}: {len(content)} bytes")
asyncio.run(main())
3.2 添加错误处理和限流
实际项目中,我们需要处理网络异常并控制并发量:
python复制async def fetch_with_retry(session, url, max_retries=3):
for attempt in range(max_retries):
try:
async with session.get(url, timeout=5) as response:
response.raise_for_status()
return await response.text()
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
if attempt == max_retries - 1:
print(f"Failed to fetch {url}: {str(e)}")
return None
await asyncio.sleep(1 * (attempt + 1))
async def worker(session, queue, results):
while True:
url = await queue.get()
try:
content = await fetch_with_retry(session, url)
if content:
results.append((url, len(content)))
finally:
queue.task_done()
async def main():
urls = [...] # 大量URL
queue = asyncio.Queue(maxsize=20) # 控制并发量
results = []
async with aiohttp.ClientSession() as session:
workers = [asyncio.create_task(worker(session, queue, results))
for _ in range(10)] # 10个工作协程
for url in urls:
await queue.put(url)
await queue.join()
for worker_task in workers:
worker_task.cancel()
await asyncio.gather(*workers, return_exceptions=True)
print(f"Fetched {len(results)} pages")
4. 常见陷阱与性能优化
4.1 阻塞操作破坏事件循环
在协程中执行CPU密集型或阻塞I/O操作会破坏事件循环的调度。常见错误:
python复制async def bad_example():
# 同步阻塞操作
time.sleep(1) # 错误!应该用await asyncio.sleep(1)
# CPU密集型计算
[x*x for x in range(10**6)] # 长时间占用事件循环
解决方案:
- 对于阻塞I/O:使用
loop.run_in_executor在线程池中运行 - 对于CPU密集型任务:考虑使用多进程
4.2 协程未正确await
忘记await协程是常见错误:
python复制async def nested():
await asyncio.sleep(1)
return 42
async def main():
# 错误:没有await
nested() # 这会创建一个协程对象但不会执行它
# 正确
result = await nested()
4.3 资源泄漏
忘记关闭异步资源会导致泄漏:
python复制async def leaky():
session = aiohttp.ClientSession() # 没有使用async with
await session.get('https://example.com')
# 忘记session.close()
正确做法是始终使用async with上下文管理器。
5. 高级模式与最佳实践
5.1 协程与生成器的结合
Asyncio协程可以结合生成器实现复杂的数据流处理:
python复制async def data_producer():
for i in range(5):
await asyncio.sleep(0.5)
yield i
async def data_consumer():
async for item in data_producer():
print(f"Processed: {item * 2}")
asyncio.run(data_consumer())
5.2 使用信号量控制并发
当需要精确控制资源访问时,可以使用asyncio.Semaphore:
python复制async def limited_resource(semaphore, id):
async with semaphore:
print(f"Resource {id} acquired")
await asyncio.sleep(1)
print(f"Resource {id} released")
async def main():
semaphore = asyncio.Semaphore(3) # 最多3个并发
tasks = [limited_resource(semaphore, i) for i in range(10)]
await asyncio.gather(*tasks)
5.3 跨线程事件循环
有时需要在非主线程中使用事件循环:
python复制def blocking_operation():
# 在另一个线程中运行事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def coro():
await asyncio.sleep(1)
return "Done"
return loop.run_until_complete(coro())
# 在主线程中调用
import threading
result = threading.Thread(target=blocking_operation).start()
6. 调试与测试技巧
6.1 调试异步代码
使用asyncio.debug模式可以获得更多信息:
python复制asyncio.run(main(), debug=True)
这会显示:
- 未被await的协程警告
- 慢回调警告(默认超过100ms)
- 任务创建和销毁的日志
6.2 单元测试异步代码
使用unittest.IsolatedAsyncioTestCase测试异步代码:
python复制import unittest
class TestAsyncFunctions(unittest.IsolatedAsyncioTestCase):
async def test_fetch(self):
result = await fetch("https://example.com")
self.assertIsInstance(result, str)
6.3 性能分析
使用cProfile分析异步代码性能:
python复制import cProfile
async def my_coroutine():
# 你的协程代码
pass
def profile_async():
asyncio.run(my_coroutine())
cProfile.run('profile_async()', sort='cumtime')
7. 实际项目中的架构建议
7.1 分层设计
良好的异步应用应该分层:
code复制应用层 (async/await)
|
业务逻辑层 (纯协程)
|
基础设施层 (aiohttp, aiomysql等)
7.2 错误处理策略
实现统一的错误处理中间件:
python复制async def error_middleware(coro, *args, **kwargs):
try:
return await coro(*args, **kwargs)
except aiohttp.ClientError as e:
print(f"Network error: {e}")
return None
except asyncio.TimeoutError:
print("Operation timed out")
return None
# 使用方式
result = await error_middleware(my_risky_coroutine, param1, param2)
7.3 配置管理
使用异步安全的配置管理:
python复制import configparser
async def load_config():
loop = asyncio.get_event_loop()
config = configparser.ConfigParser()
await loop.run_in_executor(None, config.read, 'config.ini')
return config
在大型项目中,我通常会创建一个异步应用对象来集中管理这些资源:
python复制class Application:
def __init__(self):
self.config = None
self.http_client = None
self.db_pool = None
async def startup(self):
self.config = await load_config()
self.http_client = aiohttp.ClientSession()
self.db_pool = await aiomysql.create_pool(...)
async def shutdown(self):
await self.http_client.close()
self.db_pool.close()
await self.db_pool.wait_closed()
app = Application()
asyncio.run(app.startup())
try:
asyncio.run(main(app))
finally:
asyncio.run(app.shutdown())
