9 minutes
异步编程与 asyncio
异步编程概述
asyncio 是 Python 用于编写并发代码的标准库,使用 async/await 语法。它是处理 I/O 密集型高并发任务的最佳方案——比如 Web 服务器、爬虫、API 调用等。
与多线程不同,asyncio 在单线程内实现并发,没有线程切换开销和竞态条件问题。与多进程不同,asyncio 可以轻松处理数万个并发连接。
核心概念
协程(Coroutine)
协程是可以暂停执行和恢复执行的函数,是 async/await 的基础:
import asyncio
# 定义协程函数
async def say_hello():
print("你好")
await asyncio.sleep(1) # 模拟 I/O 等待
print("世界")
# 调用协程函数返回协程对象,不会执行
coro = say_hello()
print(type(coro)) # <class 'coroutine'>
# 运行协程
asyncio.run(say_hello())
关键区别:
- 普通函数调用:
func()→ 立即执行并返回结果 - 协程函数调用:
async_func()→ 返回协程对象,需要 await 或 asyncio.run
await 关键字
await 用于等待一个 awaitable 对象(协程、Task、Future)完成:
import asyncio
async def fetch_data():
print("开始获取数据...")
await asyncio.sleep(2) # 模拟网络请求
return {"data": "some data"}
async def main():
# await 会暂停当前协程,等待 fetch_data 完成
result = await fetch_data()
print(f"结果: {result}")
asyncio.run(main())
事件循环
事件循环是 asyncio 的核心调度器,它负责管理所有协程的执行:
import asyncio
# Python 3.10+ 推荐用法
async def main():
print("协程1 开始")
await asyncio.sleep(1)
print("协程1 结束")
async def main2():
print("协程2 开始")
await asyncio.sleep(0.5)
print("协程2 结束")
# 获取事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main())
finally:
loop.close()
# 更简单的做法(Python 3.7+)
asyncio.run(main())
事件循环工作原理:
- 将协程注册到事件循环
- 事件循环按顺序执行协程
- 遇到
await时暂停当前协程,切换到其他协程 - I/O 操作完成后,恢复等待的协程
并发运行多个协程
asyncio.gather()
gather 并发执行多个协程并等待所有完成:
import asyncio
import time
async def fetch_user(user_id: int) -> dict:
"""模拟获取用户信息"""
await asyncio.sleep(1) # 模拟网络延迟
return {"id": user_id, "name": f"用户{user_id}"}
async def fetch_orders(user_id: int) -> list:
"""模拟获取用户订单"""
await asyncio.sleep(1.5)
return [{"order_id": 100 + user_id, "amount": 99.9}]
async def main():
start = time.perf_counter()
# 并发执行
user_task = fetch_user(1)
orders_task = fetch_orders(1)
# gather 等待所有协程完成
user, orders = await asyncio.gather(user_task, orders_task)
print(f"用户: {user}")
print(f"订单: {orders}")
print(f"耗时: {time.perf_counter() - start:.2f}秒")
# 耗时约 1.5 秒(而非 2.5 秒)
asyncio.run(main())
asyncio.create_task()
create_task 将协程包装为 Task 并在后台调度:
import asyncio
import time
async def slow_operation(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"{name} 完成"
async def main():
start = time.perf_counter()
# 创建任务(立即开始后台执行)
task1 = asyncio.create_task(slow_operation("任务1", 2))
task2 = asyncio.create_task(slow_operation("任务2", 1))
# 此时两个任务已在后台运行
# 等待任务完成
result1 = await task1
result2 = await task2
print(result1) # 任务1 完成
print(result2) # 任务2 完成
print(f"耗时: {time.perf_counter() - start:.2f}秒")
asyncio.run(main())
asyncio.create_task vs asyncio.gather
import asyncio
# create_task:适合需要单独管理的独立任务
async def main1():
task = asyncio.create_task(some_coro())
# 处理其他事情...
result = await task
return result
# gather:适合等待一组相关协程全部完成
async def main2():
results = await asyncio.gather(
fetch(1),
fetch(2),
fetch(3),
)
return results
awaitable 对象的三种类型
1. 协程(Coroutine)
async def my_coro():
return 42
coro = my_coro() # 协程对象
result = await coro # 可以 await
2. Task
async def my_coro():
return 42
task = asyncio.create_task(my_coro()) # Task 对象
result = await task # 可以 await
3. Future
# Future 是更低层的 awaitable
# Task 是 Future 的子类
# 通常不需要直接使用 Future
future = asyncio.Future()
# 在某个时候 future.set_result(value)
result = await future
asyncio.sleep
asyncio.sleep 是异步版本的 time.sleep,它不会阻塞线程:
import asyncio
import time
async def demo_sleep():
# BAD: 会阻塞整个线程
# time.sleep(1)
# GOOD: 只暂停当前协程,其他协程可以运行
await asyncio.sleep(1)
async def task(name: str, delay: float):
print(f"{name} 开始")
await asyncio.sleep(delay) # 非阻塞等待
print(f"{name} 结束 (等待了{delay}秒)")
async def main():
# 同时启动3个任务
await asyncio.gather(
task("A", 2),
task("B", 1),
task("C", 3),
)
start = time.perf_counter()
asyncio.run(main())
print(f"总耗时: {time.perf_counter() - start:.2f}秒")
# 输出:
# A 开始
# B 开始
# C 开始
# B 结束 (等待了1秒)
# A 结束 (等待了2秒)
# C 结束 (等待了3秒)
# 总耗时: 3.00秒(而非6秒)
异步上下文管理器
import asyncio
class AsyncResource:
"""异步资源管理器"""
async def __aenter__(self):
print("获取资源...")
await asyncio.sleep(0.5)
print("资源就绪")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("释放资源...")
await asyncio.sleep(0.3)
print("资源已释放")
async def use(self):
print("使用资源中...")
await asyncio.sleep(0.5)
async def main():
async with AsyncResource() as resource:
await resource.use()
asyncio.run(main())
使用 @asynccontextmanager
from contextlib import asynccontextmanager
@asynccontextmanager
async def database_session():
"""异步数据库会话上下文管理器"""
print("连接数据库...")
await asyncio.sleep(0.5)
session = {"connected": True}
try:
yield session # 提供会话
finally:
print("关闭数据库连接...")
await asyncio.sleep(0.3)
async def main():
async with database_session() as session:
print(f"使用数据库: {session}")
await asyncio.sleep(0.5)
asyncio.run(main())
异步迭代器
import asyncio
class AsyncRange:
"""异步版本的 range"""
def __init__(self, start, end, delay=0.5):
self.start = start
self.end = end
self.delay = delay
def __aiter__(self):
return self
async def __anext__(self):
if self.start >= self.end:
raise StopAsyncIteration
value = self.start
self.start += 1
await asyncio.sleep(self.delay) # 模拟异步操作
return value
async def main():
async for num in AsyncRange(1, 5, 0.3):
print(f"收到: {num}")
asyncio.run(main())
异步生成器
import asyncio
async def async_range(start: int, end: int, delay: float = 0.5):
"""异步生成器"""
for i in range(start, end):
await asyncio.sleep(delay)
yield i
async def main():
async for num in async_range(1, 6, 0.3):
print(f"生成: {num}")
asyncio.run(main())
asyncio.Queue
import asyncio
import random
async def producer(queue: asyncio.Queue, item_count: int):
"""生产者"""
for i in range(item_count):
item = f"数据-{i}"
await queue.put(item)
print(f"生产: {item}")
await asyncio.sleep(random.uniform(0.1, 0.3))
# 发送结束信号
await queue.put(None)
async def consumer(queue: asyncio.Queue, name: str):
"""消费者"""
while True:
item = await queue.get()
if item is None:
await queue.put(None) # 传递给其他消费者
break
print(f" {name} 消费: {item}")
await asyncio.sleep(random.uniform(0.2, 0.5))
async def main():
queue = asyncio.Queue(maxsize=5)
# 创建生产者消费者任务
tasks = [
asyncio.create_task(producer(queue, 8)),
asyncio.create_task(consumer(queue, "消费者A")),
asyncio.create_task(consumer(queue, "消费者B")),
]
await asyncio.gather(*tasks)
asyncio.run(main())
运行阻塞代码
在异步代码中,如果遇到阻塞操作,使用 run_in_executor 将其交给线程池:
import asyncio
import time
def blocking_io() -> str:
"""同步阻塞函数"""
time.sleep(2) # 假设是阻塞 I/O
return "阻塞操作结果"
async def main():
loop = asyncio.get_running_loop()
# 将阻塞函数放到线程池执行
result = await loop.run_in_executor(
None, # None = 默认线程池
blocking_io
)
print(result)
asyncio.run(main())
封装同步库为异步接口
import asyncio
import requests # 同步库
async def async_request(url: str) -> dict:
"""将同步 requests 封装为异步"""
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None,
requests.get,
url
)
# 更好的方式:直接使用 aiohttp
asyncio.wait 和 as_completed
asyncio.wait
import asyncio
async def make_request(url: str) -> str:
await asyncio.sleep(1)
return f"响应来自 {url}"
async def main():
tasks = [
asyncio.create_task(make_request(f"http://api.example.com/{i}"))
for i in range(5)
]
# 等待所有完成
done, pending = await asyncio.wait(tasks)
for task in done:
print(task.result())
# 返回结果的方式
# FIRST_COMPLETED: 任一完成就返回
# FIRST_EXCEPTION: 任一异常就返回
# ALL_COMPLETED: 全部完成(默认)
done, pending = await asyncio.wait(
tasks,
timeout=5.0,
return_when=asyncio.FIRST_COMPLETED
)
asyncio.run(main())
asyncio.as_completed
import asyncio
async def work(n: int) -> str:
await asyncio.sleep(n)
return f"任务 {n} 完成(耗时{n}秒)"
async def main():
tasks = [work(3), work(1), work(2)]
# 按完成顺序处理结果
for coro in asyncio.as_completed(tasks):
result = await coro
print(f"最先完成: {result}")
asyncio.run(main())
# 输出:
# 最先完成: 任务 1 完成(耗时1秒)
# 最先完成: 任务 2 完成(耗时2秒)
# 最先完成: 任务 3 完成(耗时3秒)
超时与取消
asyncio.wait_for
import asyncio
async def slow_operation():
await asyncio.sleep(10)
return "完成"
async def main():
try:
# 设置超时,超过时间引发 TimeoutError
result = await asyncio.wait_for(slow_operation(), timeout=2)
print(result)
except asyncio.TimeoutError:
print("操作超时!")
asyncio.run(main())
任务取消
import asyncio
async def cancellable_work():
try:
for i in range(10):
await asyncio.sleep(0.5)
print(f"工作进度: {i}")
except asyncio.CancelledError:
print("任务被取消")
raise # 必须重新抛出
async def main():
task = asyncio.create_task(cancellable_work())
await asyncio.sleep(1.5)
task.cancel() # 取消任务
try:
await task
except asyncio.CancelledError:
print("已确认取消")
asyncio.run(main())
实战:异步 Web 爬虫
import asyncio
import aiohttp
from typing import Optional
# 需要安装: pip install aiohttp
async def fetch_url(
session: aiohttp.ClientSession,
url: str,
timeout: int = 10
) -> Optional[dict]:
"""异步获取 URL 内容"""
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as response:
text = await response.text()
return {
"url": url,
"status": response.status,
"length": len(text),
"content": text[:200] # 只保留前200字符
}
except Exception as e:
return {"url": url, "error": str(e)}
async def crawl_urls(urls: list[str], max_concurrent: int = 5) -> list[dict]:
"""并发爬取多个 URL"""
connector = aiohttp.TCPConnector(limit=max_concurrent)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [
r for r in results
if isinstance(r, dict)
]
async def main():
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
"https://httpbin.org/status/200",
"https://httpbin.org/status/404",
]
import time
start = time.perf_counter()
results = await crawl_urls(urls)
elapsed = time.perf_counter() - start
print(f"爬取完成,共 {len(results)} 个结果,耗时 {elapsed:.2f}秒")
for result in results:
if "error" in result:
print(f"✗ {result['url']}: {result['error']}")
else:
print(f"✓ {result['url']}: status={result['status']}, size={result['length']}")
# asyncio.run(main())
实战:异步文件读取
import asyncio
import aiofiles
# 需要安装: pip install aiofiles
async def read_file_async(filepath: str) -> str:
"""异步读取文件"""
async with aiofiles.open(filepath, "r", encoding="utf-8") as f:
return await f.read()
async def write_file_async(filepath: str, content: str):
"""异步写入文件"""
async with aiofiles.open(filepath, "w", encoding="utf-8") as f:
await f.write(content)
async def process_files(filepaths: list[str]):
"""并发处理多个文件"""
tasks = [read_file_async(fp) for fp in filepaths]
contents = await asyncio.gather(*tasks)
# 处理内容(例如:转为大写)
processed = [content.upper() for content in contents]
# 并发写入
write_tasks = [
write_file_async(f"processed_{i}.txt", content)
for i, content in enumerate(processed)
]
await asyncio.gather(*write_tasks)
# async def main():
# await process_files(["file1.txt", "file2.txt", "file3.txt"])
# asyncio.run(main())
实战:同步 vs 异步性能对比
import asyncio
import time
# 同步版本
def sync_work(n: int) -> int:
"""模拟 I/O 操作"""
time.sleep(0.5) # 同步阻塞
return n * 2
def sync_main():
start = time.perf_counter()
results = []
for i in range(10):
result = sync_work(i)
results.append(result)
elapsed = time.perf_counter() - start
return results, elapsed
# 异步版本
async def async_work(n: int) -> int:
"""异步 I/O 操作"""
await asyncio.sleep(0.5) # 非阻塞
return n * 2
async def async_main():
start = time.perf_counter()
tasks = [asyncio.create_task(async_work(i)) for i in range(10)]
results = await asyncio.gather(*tasks)
elapsed = time.perf_counter() - start
return results, elapsed
def compare():
# 同步
sync_results, sync_time = sync_main()
print(f"同步: {sync_time:.2f}秒")
# 异步
async_results, async_time = asyncio.run(async_main())
print(f"异步: {async_time:.2f}秒")
print(f"加速比: {sync_time / async_time:.0f}x")
print(f"结果一致: {sync_results == async_results}")
compare()
# 输出:
# 同步: 5.00秒(10 × 0.5)
# 异步: 0.50秒(并发执行)
# 加速比: 10x
# 结果一致: True
常见陷阱
陷阱1:在异步函数中调用阻塞函数
import asyncio
import time
# BAD: 阻塞整个事件循环
async def bad_func():
time.sleep(5) # 阻塞了所有协程!
# GOOD: 使用 asyncio.sleep
async def good_func():
await asyncio.sleep(5)
# GOOD: 或使用 run_in_executor
async def good_func2():
await asyncio.get_running_loop().run_in_executor(None, time.sleep, 5)
陷阱2:忘记 await
import asyncio
async def fetch():
await asyncio.sleep(1)
return 42
async def main():
# BAD: 没有 await,协程不会执行
result = fetch()
print(result) # <coroutine object ...>
# GOOD: 正确 await
result = await fetch()
print(result) # 42
asyncio.run(main())
陷阱3:在事件循环外调用 async 函数
import asyncio
async def hello():
print("你好")
# BAD: 必须通过事件循环执行
# hello() # RuntimeWarning
# GOOD: 使用 asyncio.run
asyncio.run(hello()) # 正确
陷阱4:共享可变状态
import asyncio
# 虽然是单线程,但仍需注意:
# 在 await 之间,状态可能被其他协程修改
counter = 0
async def increment():
global counter
temp = counter
await asyncio.sleep(0) # 切换到其他协程!
counter = temp + 1
async def main():
await asyncio.gather(increment(), increment(), increment())
print(counter) # 可能是 1 或 2,不是 3!
# 解决方案:使用 asyncio.Lock
lock = asyncio.Lock()
async def safe_increment():
global counter
async with lock:
temp = counter
await asyncio.sleep(0)
counter = temp + 1
async def main_safe():
await asyncio.gather(safe_increment(), safe_increment(), safe_increment())
print(counter) # 3
asyncio.run(main_safe())
三种并发模型的对比
# 这是一个总结对比,不是完整可运行代码
# 1. 多线程(threading)
# 适用:I/O 密集型
# 优点:直观,适合有大量阻塞调用的场景
# 缺点:GIL 限制,有竞态条件
# 示例:网络请求、文件读写
# 2. 多进程(multiprocessing)
# 适用:CPU 密集型
# 优点:真正并行,不受 GIL 限制
# 缺点:创建开销大,通信复杂
# 示例:数值计算、图像处理
# 3. asyncio
# 适用:高并发 I/O
# 优点:单线程无锁,可处理数万连接
# 缺点:需要全栈异步,生态不完善
# 示例:Web 服务器、爬虫、API 网关
性能对比图(概念)
I/O 密集型 CPU 密集型
串行 ██ 慢 ██ 慢
多线程 ██████████ 快 ██ 慢(GIL)
多进程 ██████ 中等 ██████████ 快
asyncio ████████████ 最快 ██ 慢
Python 3.13+ 异步新特性
Python 3.13 中的 asyncio 改进:
import asyncio
# Python 3.12+: TaskGroup 简化任务管理
async def task_group_demo():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(asyncio.sleep(1))
task2 = tg.create_task(asyncio.sleep(2))
task3 = tg.create_task(asyncio.sleep(3))
# 所有任务完成或出错时自动管理
# 错误传播:TaskGroup 中任一任务失败会取消其他任务
async def task_group_error():
async with asyncio.TaskGroup() as tg:
tg.create_task(asyncio.sleep(1))
tg.create_task(asyncio.sleep(2))
tg.create_task(raise_error()) # 这个失败会取消所有任务
异步编程最佳实践
1. 使用 asyncio.run 作为入口
# Python 3.7+ 推荐
def main():
asyncio.run(async_main())
asyncio.run(main())
2. 使用 TaskGroup 管理任务
# Python 3.11+
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(task1())
tg.create_task(task2())
3. 设置合理的超时
async def fetch_with_timeout(url: str):
try:
result = await asyncio.wait_for(fetch(url), timeout=5)
return result
except asyncio.TimeoutError:
return None
4. 使用信号量控制并发量
semaphore = asyncio.Semaphore(10) # 最多10个并发
async def limited_request(url: str):
async with semaphore:
return await fetch(url)
5. 避免在协程中混用同步 I/O
# BAD
async def bad():
file = open("data.txt") # 同步操作,阻塞事件循环
...
# GOOD
async def good():
async with aiofiles.open("data.txt") as f:
...
小结
asyncio 是 Python 异步编程的核心,它通过单线程事件循环实现了高效的 I/O 并发。本章我们系统学习了协程与 async/await 语法、事件循环的工作原理、并发运行协程的三种方式(gather、create_task、as_completed)、awaitable 对象的三种类型、异步上下文管理器和异步迭代器、asyncio.Queue 的生产者-消费者模式、超时与任务取消机制。通过异步爬虫和性能对比等实战案例,你应该已经清楚 asyncio 的适用场景和优势。最后记住:asyncio 适合高并发 I/O 密集型任务,CPU 密集型用多进程,简单 I/O 用多线程——选择合适的工具,才能写出高效的并发程序。
Summary: async/await、事件循环、协程并发、TaskGroup 与异步实战。