1. Python异步编程与Asyncio核心概念
当我们需要处理大量I/O密集型任务时,传统的同步编程方式会导致程序大部分时间都在等待I/O操作完成。异步编程通过非阻塞的方式让程序在等待I/O时可以处理其他任务,显著提升效率。
Python中的asyncio库提供了完整的异步编程框架,其核心是事件循环(event loop)机制。事件循环负责调度和执行协程(coroutine),当一个协程遇到I/O操作时,会自动挂起并将控制权交还给事件循环,事件循环会立即执行其他就绪的协程。
重要提示:异步编程并不适合CPU密集型任务,因为Python的全局解释器锁(GIL)会限制多线程执行CPU密集型任务。对于这类任务,建议考虑多进程方案。
1.1 协程与普通函数的区别
协程(coroutine)是异步编程的基本单位,使用async def定义。与普通函数不同,调用协程不会立即执行其代码,而是返回一个协程对象。这个对象需要被事件循环调度才会真正执行。
python复制import asyncio
async def my_coroutine():
print("协程开始执行")
await asyncio.sleep(1)
print("协程执行结束")
# 调用协程返回的是协程对象,不会立即执行
coro = my_coroutine()
print(type(coro)) # <class 'coroutine'>
# 需要事件循环来运行协程
asyncio.run(coro)
协程内部可以使用await表达式挂起自身,将控制权交还给事件循环。await后面通常跟着另一个协程或异步I/O操作。
1.2 事件循环的工作原理
事件循环是asyncio的核心,它负责:
- 维护一个任务队列
- 执行就绪的任务
- 当任务遇到I/O操作时挂起它
- I/O完成后将任务重新放入队列
- 重复上述过程直到所有任务完成
python复制import asyncio
async def task1():
print("任务1开始")
await asyncio.sleep(2)
print("任务1结束")
async def task2():
print("任务2开始")
await asyncio.sleep(1)
print("任务2结束")
async def main():
await asyncio.gather(task1(), task2())
asyncio.run(main())
在这个例子中,虽然task1先开始,但由于它的sleep时间更长,task2会先完成。这就是事件循环的调度效果。
2. Asyncio核心API详解
2.1 创建和运行协程
asyncio提供了几种运行协程的方式:
- asyncio.run() - 运行顶层协程
- 手动管理事件循环
- 使用asyncio.create_task()创建任务
python复制import asyncio
async def fetch_data():
print("开始获取数据")
await asyncio.sleep(2)
print("数据获取完成")
return {"data": 123}
# 方法1: asyncio.run()
result = asyncio.run(fetch_data())
print(result)
# 方法2: 手动管理事件循环
async def main():
task = asyncio.create_task(fetch_data())
await task
print(task.result())
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
实践经验:在大多数情况下,使用asyncio.run()就足够了。只有在需要更精细控制事件循环生命周期时,才考虑手动管理。
2.2 并发执行多个协程
asyncio.gather()和asyncio.wait()是并发运行多个协程的两种主要方式。
python复制import asyncio
async def worker(name, delay):
print(f"{name}开始工作")
await asyncio.sleep(delay)
print(f"{name}工作完成")
return name
async def main():
# 使用gather
results = await asyncio.gather(
worker("A", 2),
worker("B", 1),
worker("C", 3)
)
print(f"gather结果: {results}")
# 使用wait
tasks = [asyncio.create_task(worker(f"任务{i}", i)) for i in range(1, 4)]
done, pending = await asyncio.wait(tasks)
for task in done:
print(f"wait结果: {task.result()}")
asyncio.run(main())
两者的区别:
- gather会等待所有任务完成,并返回结果列表
- wait可以设置超时,返回已完成和未完成的任务集合
- gather保持任务顺序,wait不保证顺序
2.3 异步I/O操作
asyncio提供了多种异步I/O原语:
- 网络I/O:asyncio.open_connection(), asyncio.start_server()
- 子进程:asyncio.create_subprocess_exec()
- 文件I/O:aiofiles第三方库
- 定时器:asyncio.sleep()
python复制import asyncio
async def tcp_echo_client(message):
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
print(f"发送: {message}")
writer.write(message.encode())
await writer.drain()
data = await reader.read(100)
print(f"收到: {data.decode()}")
writer.close()
await writer.wait_closed()
asyncio.run(tcp_echo_client('Hello World!'))
3. 实际项目中的应用模式
3.1 异步Web请求
使用aiohttp库可以高效地发起多个HTTP请求:
python复制import aiohttp
import asyncio
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://www.python.org',
'https://www.google.com'
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for url, content in zip(urls, results):
print(f"{url}返回了{len(content)}字节")
asyncio.run(main())
3.2 数据库异步访问
使用asyncpg访问PostgreSQL数据库:
python复制import asyncpg
import asyncio
async def run_query():
conn = await asyncpg.connect(user='user', password='password',
database='database', host='127.0.0.1')
# 执行简单查询
result = await conn.fetch('SELECT * FROM users WHERE id = $1', 1)
print(result)
# 批量插入
await conn.executemany(
"INSERT INTO users(name, age) VALUES($1, $2)",
[('Alice', 25), ('Bob', 30)]
)
await conn.close()
asyncio.run(run_query())
3.3 实现WebSocket服务器
python复制import asyncio
import websockets
async def echo(websocket, path):
async for message in websocket:
print(f"收到消息: {message}")
await websocket.send(f"你发送了: {message}")
async def main():
async with websockets.serve(echo, "localhost", 8765):
await asyncio.Future() # 永久运行
asyncio.run(main())
4. 性能优化与调试技巧
4.1 测量协程执行时间
python复制import asyncio
import time
async def task():
await asyncio.sleep(1)
return "完成"
async def main():
start = time.monotonic()
result = await task()
end = time.monotonic()
print(f"任务返回: {result}, 耗时: {end - start:.2f}秒")
asyncio.run(main())
4.2 限制并发数量
使用信号量(Semaphore)控制最大并发数:
python复制import asyncio
async def worker(sem, name):
async with sem:
print(f"{name}开始工作")
await asyncio.sleep(1)
print(f"{name}工作完成")
async def main():
sem = asyncio.Semaphore(3) # 最多3个并发
tasks = [worker(sem, f"Worker-{i}") for i in range(10)]
await asyncio.gather(*tasks)
asyncio.run(main())
4.3 常见问题排查
-
协程没有被执行:
- 确保调用了asyncio.run()或通过事件循环调度
- 检查是否遗漏了await关键字
-
程序卡住不退出:
- 检查是否有未完成的任务
- 确保所有连接和资源都已正确关闭
-
性能不如预期:
- 确认任务确实是I/O密集型而非CPU密集型
- 检查是否有阻塞操作混在协程中
调试技巧:使用asyncio.debug=True可以启用调试模式,会输出更多信息帮助诊断问题。
5. 高级模式与最佳实践
5.1 协程与线程池结合
对于既有I/O又有CPU密集型操作的场景,可以结合线程池:
python复制import asyncio
import concurrent.futures
def cpu_bound(number):
return sum(i * i for i in range(number))
async def main():
loop = asyncio.get_running_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(pool, cpu_bound, 100000)
print(f"计算结果: {result}")
asyncio.run(main())
5.2 异步上下文管理器
实现__aenter__和__aexit__方法创建异步上下文管理器:
python复制import asyncio
class AsyncConnection:
async def __aenter__(self):
print("建立连接")
await asyncio.sleep(1)
return self
async def __aexit__(self, exc_type, exc, tb):
print("关闭连接")
await asyncio.sleep(1)
async def query(self):
print("执行查询")
await asyncio.sleep(1)
return "结果"
async def main():
async with AsyncConnection() as conn:
result = await conn.query()
print(result)
asyncio.run(main())
5.3 结构化并发
使用asyncio.TaskGroup(Python 3.11+)管理任务生命周期:
python复制import asyncio
async def worker(name):
print(f"{name}开始")
await asyncio.sleep(1)
print(f"{name}结束")
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(worker("任务1"))
tg.create_task(worker("任务2"))
tg.create_task(worker("任务3"))
print("所有任务完成")
asyncio.run(main())
在实际项目中,我发现合理设置超时非常重要。以下是一个带超时的请求处理模式:
python复制import asyncio
import aiohttp
async def fetch_with_timeout(url, timeout=5):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=timeout) as response:
return await response.text()
except asyncio.TimeoutError:
print(f"请求超时: {url}")
return None
async def main():
url = "https://example.com"
result = await asyncio.wait_for(fetch_with_timeout(url), timeout=10)
print(f"获取到{len(result)}字节数据" if result else "请求失败")
asyncio.run(main())
对于需要重试的场景,可以结合backoff库实现指数退避重试:
python复制import asyncio
import backoff
import aiohttp
@backoff.on_exception(backoff.expo, aiohttp.ClientError, max_tries=3)
async def fetch_with_retry(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
response.raise_for_status()
return await response.text()
async def main():
try:
data = await fetch_with_retry("https://example.com")
print(f"获取数据成功: {len(data)}字节")
except aiohttp.ClientError as e:
print(f"最终请求失败: {e}")
asyncio.run(main())
最后,分享一个我在实际项目中的经验:当设计异步API时,尽量保持接口的纯净性,避免混合同步和异步代码。如果必须提供同步接口,可以使用asyncio.run()包装,但要明确文档说明这会创建新的事件循环。
