Python 的
asyncio是解决 I/O 密集型任务的利器。本文不堆砌概念,而是带你从"为什么需要异步"到"写出能处理万级并发的服务",走完完整路径。
目录
- 为什么传统同步代码不够用
- 核心概念:协程、事件循环与 Task
- async 与 await:语法揭秘
- asyncio 核心 API 实战
- 异步上下文管理器与迭代器
- 与同步代码共存:run_in_executor
- 实战 1:高并发网络爬虫
- 实战 2:异步 Web 服务(FastAPI + asyncio)
- 实战 3:异步数据库访问
- 常见陷阱与调试技巧
- asyncio API 速查表
1. 为什么传统同步代码不够用
假设你要下载 100 个网页:
同步写法(慢)
import requests
import time
def fetch_sync(urls):
results = []
for url in urls:
resp = requests.get(url) # 阻塞!CPU 干等着网络响应
results.append(resp.text)
return results
urls = [f"https://httpbin.org/get?i={i}" for i in range(10)]
start = time.perf_counter()
fetch_sync(urls)
print(f"同步耗时: {time.perf_counter() - start:.2f}s")
# → 约 15-20 秒(100个网页则 150-200 秒!)
问题在哪?requests.get() 发送请求后,程序什么都不做,干等着网络返回。CPU 利用率几乎为零。
异步写法(快 10-100 倍)
import asyncio
import aiohttp
import time
async def fetch_one(session, url):
async with session.get(url) as resp:
return await resp.text()
async def fetch_async(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks)
urls = [f"https://httpbin.org/get?i={i}" for i in range(10)]
start = time.perf_counter()
asyncio.run(fetch_async(urls))
print(f"异步耗时: {time.perf_counter() - start:.2f}s")
# → 约 1-2 秒(100 个网页约 5-10 秒)
核心差异:当一个请求发送出去等待响应时,asyncio 立刻切换到处理下一个请求。单线程即可实现"伪并行"。
适用场景对比
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| I/O 密集型(网络请求、文件读写) | ✅ asyncio | CPU等待时还能干别的 |
| CPU 密集型(大量计算、数据处理) | ❌ multiprocessing | GIL 限制,需真并行 |
| 混合场景 | ✅ asyncio + ProcessPoolExecutor | 各自发挥长处 |
2. 核心概念:协程、事件循环与 Task
2.1 协程(Coroutine)
协程是一种"可以在运行中暂停和恢复"的函数:
import asyncio
async def hello():
print("开始")
await asyncio.sleep(1) # 暂停!把控制权交还事件循环
print("1秒后")
# hello() 被调用时,不会立即执行,而是返回一个协程对象
coro = hello()
print(type(coro)) # <class 'coroutine'>
# 需要事件循环来驱动它
asyncio.run(coro) # Python 3.7+ 推荐写法
关键理解:协程不会自己跑,它只是一个"待执行的任务描述"。
2.2 事件循环(Event Loop)
事件循环是那个"调度员":
┌─────────────────────────┐
│ Event Loop │ ← 调度员,只有一个
├─────────────────────────┤
│ 待执行任务队列 │
│ [Task A] → [Task B] │
├─────────────────────────┤
│ I/O 完成回调 │
│ Task A 的网络响应到了 │
├─────────────────────────┤
│ 执行当前任务 │
│ → 遇到 await,挂起 │
│ → 切换到下一个任务 │
└─────────────────────────┘
import asyncio
async def main():
print("开始事件循环")
await asyncio.sleep(0.5)
print("结束")
# 获取当前事件循环(低层 API,通常不需要手动使用)
loop = asyncio.get_event_loop()
# Python 3.7+ 高层 API:自动创建、运行、关闭
asyncio.run(main())
2.3 Task:协程的包装器
Task 是协程的"包装纸",它让协程被主动调度:
import asyncio
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
# task1 = say_after(1, "hello") # ❌ 这只是协程对象,没被调度
task1 = asyncio.create_task(say_after(1, "你好"))
task2 = asyncio.create_task(say_after(2, "世界"))
print(f"创建于 {asyncio.get_event_loop().time():.2f}")
await task1 # 等待任务1完成
await task2 # 等待任务2完成
asyncio.run(main())
# 总耗时约 2 秒(不是 3 秒,因为它们是并发运行的)
Task vs 裸协程:
| 操作 | await coro() | await asyncio.create_task(coro()) |
|---|---|---|
| 调度时机 | 顺序执行 | 立即注册到事件循环 |
| 并发能力 | 串行 | 并行 |
| 获取结果 | 直接返回 | task.result() |
| 取消任务 | ❌ | task.cancel() |
3. async 与 await:语法揭秘
3.1 基本规则
import asyncio
# async def 定义协程
async def fetch_data():
# await 只能出现在 async def 内部
await asyncio.sleep(1) # 暂停,让出 CPU
return "数据"
async def main():
# await 表达式的结果是协程的返回值
result = await fetch_data()
print(result) # 数据
asyncio.run(main())
3.2 await 后面的东西
await 后面必须是 Awaitable(可等待对象):
- 协程(
async def的返回值) - Task(
asyncio.create_task()的返回值) - Future(低层对象,通常由库创建)
async def demo():
# ✅ 合法
await asyncio.sleep(1) # 协程
await asyncio.create_task(coro) # Task
await some_future # Future
# ❌ 不合法
# await 123 # 错误!
# await [coro1, coro2] # list 不是 Awaitable
3.3 在普通函数中调用协程
import asyncio
async def async_func():
await asyncio.sleep(1)
return "done"
# 方式 1:asyncio.run()(最顶层入口)
asyncio.run(async_func())
# 方式 2:在已有事件循环中获取结果
async def main():
result = await async_func()
return result
# 方式 3:同步代码中运行协程(注意:不要在已有 loop 中调用!)
# result = asyncio.run(async_func())
4. asyncio 核心 API 实战
4.1 并发执行:gather
import asyncio
async def fetch_url(url):
await asyncio.sleep(1) # 模拟网络请求
return f"Result of {url}"
async def main():
urls = ["url1", "url2", "url3", "url4", "url5"]
# gather:并发运行所有协程,等待全部完成
# 约 1 秒完成(不是 5 秒!)
results = await asyncio.gather(
*[fetch_url(url) for url in urls]
)
for r in results:
print(r)
asyncio.run(main())
gather 的错误处理:
async def may_fail(x):
if x == 2:
raise ValueError(f"Error at {x}")
await asyncio.sleep(0.1)
return x * x
async def main():
# 默认:一个异常,全部取消
try:
results = await asyncio.gather(
may_fail(1), may_fail(2), may_fail(3)
)
except ValueError as e:
print(f"捕获异常: {e}")
# return_exceptions=True:返回异常而非抛出
results = await asyncio.gather(
may_fail(1), may_fail(2), may_fail(3),
return_exceptions=True
)
print(results) # [1, ValueError('Error at 2'), 9]
asyncio.run(main())
4.2 等待任一完成:wait
import asyncio
async def slow_task(name, delay):
await asyncio.sleep(delay)
return f"{name} completed"
async def main():
tasks = [
asyncio.create_task(slow_task("A", 2)),
asyncio.create_task(slow_task("B", 1)),
asyncio.create_task(slow_task("C", 3)),
]
# return_when=ALL_COMPLETED(默认)
# return_when=FIRST_COMPLETED —— 任意一个完成就返回
done, pending = await asyncio.wait(
tasks,
return_when=asyncio.FIRST_COMPLETED
)
for task in done:
print(f"已完成: {task.result()}")
# 别忘了取消剩余任务
for task in pending:
task.cancel()
asyncio.run(main())
4.3 超时控制:wait_for 和 timeout
import asyncio
async def slow_operation():
await asyncio.sleep(10)
return "Done"
async def main():
# 方式 1:wait_for(超时抛异常)
try:
result = await asyncio.wait_for(slow_operation(), timeout=2.0)
except asyncio.TimeoutError:
print("操作超时!")
# 方式 2:timeout 上下文管理器(Python 3.11+)
try:
async with asyncio.timeout(2.0):
result = await slow_operation()
except TimeoutError:
print("操作超时!")
asyncio.run(main())
4.4 限制并发数:Semaphore
import asyncio
# 同时最多 3 个并发
sem = asyncio.Semaphore(3)
async def fetch_with_limit(url):
async with sem: # 获取信号量,超限时等待
print(f"开始下载 {url}")
await asyncio.sleep(1)
print(f"完成下载 {url}")
return f"Content of {url}"
async def main():
urls = [f"https://example.com/{i}" for i in range(10)]
results = await asyncio.gather(*[fetch_with_limit(url) for url in urls])
print(f"共下载 {len(results)} 个页面")
asyncio.run(main())
4.5 队列:生产者-消费者模式
import asyncio
async def producer(queue, n):
for i in range(n):
await asyncio.sleep(0.1)
await queue.put(i)
print(f"生产: {i}")
await queue.put(None) # 哨兵,表示结束
async def consumer(queue, name):
while True:
item = await queue.get()
if item is None:
break
await asyncio.sleep(0.2)
print(f"消费者{name} 处理: {item}")
queue.task_done()
async def main():
queue = asyncio.Queue(maxsize=5)
prod_task = asyncio.create_task(producer(queue, 10))
cons_tasks = [
asyncio.create_task(consumer(queue, i))
for i in range(2) # 2 个消费者
]
await prod_task
await queue.join() # 等待所有任务处理完
for t in cons_tasks:
t.cancel()
asyncio.run(main())
5. 异步上下文管理器与迭代器
5.1 async with(异步上下文管理器)
import asyncio
class AsyncConnection:
"""模拟一个需要异步打开和关闭的资源"""
async def __aenter__(self):
print("异步连接建立...")
await asyncio.sleep(0.5)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("异步连接关闭...")
await asyncio.sleep(0.5)
async def main():
async with AsyncConnection() as conn:
print("使用连接中...")
asyncio.run(main())
5.2 async for(异步迭代器)
import asyncio
import random
class AsyncPageFetcher:
"""异步分页获取数据"""
def __init__(self, total_pages):
self.total_pages = total_pages
def __aiter__(self):
self.current = 0
return self
async def __anext__(self):
if self.current >= self.total_pages:
raise StopAsyncIteration
await asyncio.sleep(random.uniform(0.1, 0.3))
self.current += 1
return f"第 {self.current} 页数据"
async def main():
fetcher = AsyncPageFetcher(5)
async for page_data in fetcher:
print(page_data)
asyncio.run(main())
6. 与同步代码共存:run_in_executor
现实项目中总有遗留的同步代码,无法全部改为 async。run_in_executor 可以在异步代码中调用同步函数:
import asyncio
import time
def cpu_intensive(n):
"""模拟 CPU 密集型同步操作"""
count = 0
for i in range(n):
count += i ** 2
return count
async def main():
loop = asyncio.get_event_loop()
# 在线程池中运行同步函数(不阻塞事件循环)
result = await loop.run_in_executor(None, cpu_intensive, 10_000_000)
print(f"计算结果: {result}")
# 多个 CPU 密集型任务并行
with concurrent.futures.ProcessPoolExecutor() as pool:
tasks = [
loop.run_in_executor(pool, cpu_intensive, n)
for n in [5_000_000, 5_000_000, 5_000_000]
]
results = await asyncio.gather(*tasks)
print(f"并行结果: {results}")
import concurrent.futures
asyncio.run(main())
7. 实战 1:高并发网络爬虫
import asyncio
import aiohttp
import time
from urllib.parse import urljoin, urlparse
class AsyncCrawler:
"""异步爬虫:并发抓取网页并提取链接"""
def __init__(self, base_url, max_concurrent=10, max_depth=2):
self.base_url = base_url
self.max_concurrent = max_concurrent
self.max_depth = max_depth
self.visited = set()
self.results = []
async def fetch(self, session, url, semaphore):
"""获取单个页面"""
async with semaphore:
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status == 200:
return await resp.text()
except Exception as e:
print(f"抓取失败 {url}: {e}")
return None
async def crawl(self):
"""主爬取逻辑"""
semaphore = asyncio.Semaphore(self.max_concurrent)
async with aiohttp.ClientSession() as session:
html = await self.fetch(session, self.base_url, semaphore)
if html:
self.results.append({
"url": self.base_url,
"length": len(html),
"status": "ok"
})
return self.results
async def main():
urls = [
"https://httpbin.org/get",
"https://httpbin.org/ip",
"https://httpbin.org/user-agent",
] * 5 # 15 个请求
semaphore = asyncio.Semaphore(10)
async with aiohttp.ClientSession() as session:
tasks = [
fetch_one(session, url, semaphore)
for url in urls
]
start = time.perf_counter()
results = await asyncio.gather(*tasks, return_exceptions=True)
elapsed = time.perf_counter() - start
success = sum(1 for r in results if isinstance(r, str))
print(f"✅ 成功: {success}/{len(urls)}")
print(f"⏱ 耗时: {elapsed:.2f}s")
print(f"⚡ 平均: {elapsed/len(urls):.3f}s/请求")
async def fetch_one(session, url, sem):
async with sem:
async with session.get(url) as resp:
return await resp.text()
asyncio.run(main())
8. 实战 2:异步 Web 服务(FastAPI + asyncio)
from fastapi import FastAPI, HTTPException
import asyncio
import httpx
app = FastAPI(title="异步聚合 API")
# 异步 HTTP 客户端(比 requests 更适合 async 环境)
async_client = httpx.AsyncClient(timeout=10.0)
@app.get("/")
async def root():
return {"message": "异步服务运行中"}
@app.get("/aggregate")
async def aggregate_data():
"""
并发请求多个外部 API,聚合结果返回
"""
urls = {
"ip": "https://httpbin.org/ip",
"user_agent": "https://httpbin.org/user-agent",
"headers": "https://httpbin.org/headers",
}
# 并发请求所有 API
tasks = {
name: asyncio.create_task(async_client.get(url))
for name, url in urls.items()
}
results = {}
for name, task in tasks.items():
try:
resp = await task
results[name] = resp.json()
except Exception as e:
results[name] = {"error": str(e)}
return {
"status": "success",
"data": results
}
@app.get("/slow")
async def slow_endpoint():
"""模拟耗时操作"""
await asyncio.sleep(2)
return {"message": "慢操作完成"}
@app.on_event("shutdown")
async def shutdown():
await async_client.aclose()
# 启动:uvicorn main:app --reload
9. 实战 3:异步数据库访问
import asyncio
import asyncpg # pip install asyncpg
# PostgreSQL 异步连接
async def demo_db():
conn = await asyncpg.connect("postgresql://user:pass@localhost/db")
# 执行查询
rows = await conn.fetch("SELECT * FROM users WHERE active = $1", True)
for row in rows:
print(row["name"], row["email"])
# 事务
async with conn.transaction():
await conn.execute(
"INSERT INTO users (name, email) VALUES ($1, $2)",
"Alice", "alice@example.com"
)
await conn.close()
# 连接池(生产环境推荐)
async def demo_pool():
pool = await asyncpg.create_pool(
"postgresql://user:pass@localhost/db",
min_size=10,
max_size=20
)
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT * FROM users WHERE id = $1", 1)
print(row)
await pool.close()
# asyncio.run(demo_pool())
异步 ORM(Tortoise ORM / SQLAlchemy async)
# Tortoise ORM 示例
from tortoise import Tortoise, fields
from tortoise.models import Model
class User(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
email = fields.CharField(max_length=255, unique=True)
created_at = fields.DatetimeField(auto_now_add=True)
async def init():
await Tortoise.init(
db_url="sqlite://db.sqlite3",
modules={"models": ["__main__"]}
)
await Tortoise.generate_schemas()
async def create_user():
user = await User.create(name="Alice", email="alice@example.com")
print(f"创建用户: {user.id}")
users = await User.filter(name__startswith="A").all()
print(f"找到 {len(users)} 个用户")
10. 常见陷阱与调试技巧
陷阱 1:在协程中调用同步阻塞 I/O
# ❌ 错误!阻塞了整个事件循环
async def bad():
requests.get("https://example.com") # requests 是同步库
time.sleep(1) # 阻塞睡眠
# ✅ 正确:使用异步库
async def good():
async with aiohttp.ClientSession() as session:
async with session.get("https://example.com") as resp:
text = await resp.text()
await asyncio.sleep(1)
陷阱 2:忘记 await
async def task():
await asyncio.sleep(1)
return "done"
async def main():
result = task() # ❌ result 是协程对象,不是 "done"!
print(result) # <coroutine object task at ...>
result = await task() # ✅ 正确
print(result) # done
陷阱 3:asyncio.run() 嵌套调用
# ❌ asyncio.run() 不能嵌套
async def main():
asyncio.run(some_other()) # RuntimeError!
# ✅ 正确:用 await
async def main():
await some_other()
陷阱 4:异常被静默吞掉
async def failing_task():
await asyncio.sleep(1)
raise ValueError("出错了")
async def main():
task = asyncio.create_task(failing_task())
# 如果不 await task,异常可能被静默忽略或延迟抛到事件循环关闭时
# ✅ 正确做法:
try:
await task
except ValueError as e:
print(f"捕获: {e}")
# 或:
task.add_done_callback(lambda t: print(f"任务结束: {t.exception()}"))
asyncio.run(main())
调试技巧
# 开启 asyncio 调试模式
asyncio.run(main(), debug=True)
# 设置慢协程警告阈值(默认 100ms)
import warnings
warnings.filterwarnings("default", category=RuntimeWarning)
# 查看事件循环中所有待处理任务
async def debug_tasks():
tasks = asyncio.all_tasks()
for t in tasks:
print(f"任务: {t.get_name()}, 完成: {t.done()}")
11. asyncio API 速查表
核心函数
| 函数 | 作用 | 示例 |
|---|---|---|
asyncio.run() | 运行协程并管理事件循环 | asyncio.run(main()) |
asyncio.create_task() | 创建 Task(立即调度) | task = asyncio.create_task(coro()) |
asyncio.gather() | 并发执行多个协程 | await asyncio.gather(*tasks) |
asyncio.wait() | 等待任务集合 | done, pending = await asyncio.wait(tasks) |
asyncio.wait_for() | 带超时等待 | await asyncio.wait_for(coro, timeout=5) |
asyncio.sleep() | 非阻塞睡眠 | await asyncio.sleep(1) |
asyncio.shield() | 保护任务不被取消 | await asyncio.shield(task) |
并发控制
| 类 / 函数 | 作用 |
|---|---|
asyncio.Semaphore(n) | 限制同时 n 个并发 |
asyncio.Lock() | 互斥锁 |
asyncio.Event() | 事件通知 |
asyncio.Condition() | 条件变量 |
asyncio.Queue() | 异步队列 |
执行器
| 函数 | 作用 |
|---|---|
loop.run_in_executor(None, fn) | 线程池运行同步函数 |
loop.run_in_executor(ProcessPoolExecutor(), fn) | 进程池运行 CPU 密集型函数 |
延伸阅读
- Python 并发与性能深度指南 —— GIL、多进程与性能分析
- Python Web 框架:FastAPI 实战 —— async/await 在 Web 开发中的应用
- Python 内存管理、垃圾回收与性能调优 —— asyncio 程序的性能优化
- Node.js 事件循环对比 —— 不同语言的事件循环实现差异
asyncio 是 Python 3.4+ 带来的革命性特性。掌握它,你就掌握了用单线程写出高并发程序的能力。记住核心口诀:async 定义,await 等待,gather 并发,Semaphore 限流。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。