📚 前言
在现代 Python 开发中,异步编程已经成为处理高并发场景的标配技术。本文将带你从零开始掌握 Python 协程,通过丰富的代码示例和实战场景,让你彻底理解 async/await 的魔法。
一、为什么需要协程?
1.1 传统并发方案的局限
在 Python 中,我们有三种主要的并发方案:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 多线程 | I/O 密集型任务 | 共享内存,切换开销小 | GIL 限制,无法利用多核 CPU |
| 多进程 | CPU 密集型任务 | 真正并行,绕过 GIL | 内存开销大,进程间通信复杂 |
| 协程 | 高并发 I/O 密集型 | 极低开销,单线程高并发 | 不适合 CPU 密集型任务 |
1.2 协程的核心优势
python
# 传统同步代码:串行执行,总耗时 = 各任务耗时之和
import time
def download_file(name):
print(f"开始下载 {name}")
time.sleep(2) # 模拟网络 I/O
print(f"{name} 下载完成")
start = time.time()
download_file("文件A")
download_file("文件B")
download_file("文件C")
print(f"总耗时: {time.time() - start:.2f}秒") # 输出约 6 秒
问题:三个文件串行下载,总耗时 6 秒,但实际上网络 I/O 期间 CPU 是空闲的!
📌 协程的解决方案:在等待 I/O 时主动让出 CPU,让其他任务执行,实现"伪并行"。
二、协程核心概念
2.1 关键术语解析
1️⃣ 协程函数(Coroutine Function)
使用 async def 定义的函数:
python
async def my_coroutine():
return "Hello Coroutine"
2️⃣ 协程对象(Coroutine Object)
调用协程函数返回的对象(不会立即执行):
python
coro = my_coroutine() # 此时函数体还未执行
print(type(coro)) # <class 'coroutine'>
3️⃣ 事件循环(Event Loop)
协程的调度器,负责执行协程并管理 I/O 事件:
python
import asyncio
asyncio.run(my_coroutine()) # 创建事件循环并运行协程
4️⃣ 可等待对象(Awaitable)
可以被 await 的对象,包括:
- 协程对象
- Task 对象(
asyncio.create_task()创建) - Future 对象
三、渐进式代码示例
3.1 基础示例:第一个协程
python
import asyncio
async def say_hello(name):
"""一个简单的协程函数"""
print(f"Hello, {name}!")
await asyncio.sleep(1) # 模拟异步操作(必须用 asyncio.sleep)
print(f"Goodbye, {name}!")
# 运行协程的三种方式
# 方式1:推荐(Python 3.7+)
asyncio.run(say_hello("Alice"))
# 方式2:手动管理事件循环(旧版本)
# loop = asyncio.get_event_loop()
# loop.run_until_complete(say_hello("Bob"))
# 方式3:在已有事件循环中运行(Jupyter Notebook)
# await say_hello("Charlie")
运行结果:
Hello, Alice!
(等待 1 秒)
Goodbye, Alice!
⚠️ 注意 :
await只能在async def函数内部使用,否则会报SyntaxError。
3.2 并发示例:同时执行多个协程
方法一:使用 asyncio.gather()
python
import asyncio
import time
async def fetch_data(db_id):
"""模拟从数据库获取数据"""
print(f"[{time.strftime('%H:%M:%S')}] 开始查询数据库 {db_id}")
await asyncio.sleep(2) # 模拟查询耗时
print(f"[{time.strftime('%H:%M:%S')}] 数据库 {db_id} 查询完成")
return f"数据_{db_id}"
async def main():
start = time.time()
# 并发执行三个协程
results = await asyncio.gather(
fetch_data(1),
fetch_data(2),
fetch_data(3)
)
print(f"获取结果: {results}")
print(f"总耗时: {time.time() - start:.2f}秒")
asyncio.run(main())
运行结果:
[14:23:10] 开始查询数据库 1
[14:23:10] 开始查询数据库 2
[14:23:10] 开始查询数据库 3
[14:23:12] 数据库 1 查询完成
[14:23:12] 数据库 2 查询完成
[14:23:12] 数据库 3 查询完成
获取结果: ['数据_1', '数据_2', '数据_3']
总耗时: 2.01秒 # 注意:不是 6 秒!
方法二:使用 asyncio.create_task()
python
async def main():
start = time.time()
# 创建任务(立即开始执行)
task1 = asyncio.create_task(fetch_data(1))
task2 = asyncio.create_task(fetch_data(2))
task3 = asyncio.create_task(fetch_data(3))
# 等待所有任务完成
result1 = await task1
result2 = await task2
result3 = await task3
print(f"结果: {result1}, {result2}, {result3}")
print(f"总耗时: {time.time() - start:.2f}秒")
asyncio.run(main())
📌 小贴士 :
gather()适合批量执行,create_task()适合需要单独控制任务的场景。
3.3 实战示例:异步 HTTP 请求
场景:批量爬取网页
python
import asyncio
import aiohttp # 需要安装:pip install aiohttp
import time
async def fetch_url(session, url):
"""异步获取单个 URL 的内容"""
try:
async with session.get(url, timeout=10) as response:
content = await response.text()
print(f"✅ {url} - 状态码: {response.status}, 长度: {len(content)}")
return content
except Exception as e:
print(f"❌ {url} - 错误: {e}")
return None
async def main():
urls = [
"https://www.python.org",
"https://www.github.com",
"https://www.stackoverflow.com",
"https://www.reddit.com",
"https://www.wikipedia.org"
]
start = time.time()
# 创建共享的 HTTP 会话
async with aiohttp.ClientSession() as session:
# 并发请求所有 URL
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
print(f"\n总耗时: {time.time() - start:.2f}秒")
print(f"成功获取: {sum(1 for r in results if r)} 个页面")
asyncio.run(main())
运行结果示例:
✅ https://www.github.com - 状态码: 200, 长度: 245678
✅ https://www.python.org - 状态码: 200, 长度: 123456
✅ https://www.stackoverflow.com - 状态码: 200, 长度: 345678
✅ https://www.reddit.com - 状态码: 200, 长度: 456789
✅ https://www.wikipedia.org - 状态码: 200, 长度: 234567
总耗时: 1.85秒 # 如果用 requests 同步请求,可能需要 5-10 秒
成功获取: 5 个页面
3.4 进阶示例:异步数据库操作
python
import asyncio
import aiosqlite # 需要安装:pip install aiosqlite
async def create_table(db_path):
"""创建数据表"""
async with aiosqlite.connect(db_path) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
""")
await db.commit()
print("✅ 数据表创建成功")
async def insert_user(db_path, user_id, name, email):
"""插入用户数据"""
async with aiosqlite.connect(db_path) as db:
await db.execute(
"INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
(user_id, name, email)
)
await db.commit()
print(f"✅ 插入用户: {name}")
async def query_users(db_path):
"""查询所有用户"""
async with aiosqlite.connect(db_path) as db:
async with db.execute("SELECT * FROM users") as cursor:
rows = await cursor.fetchall()
print("\n📋 用户列表:")
for row in rows:
print(f" ID: {row[0]}, 姓名: {row[1]}, 邮箱: {row[2]}")
async def main():
db_path = "test.db"
# 创建表
await create_table(db_path)
# 并发插入多个用户
await asyncio.gather(
insert_user(db_path, 1, "Alice", "alice@example.com"),
insert_user(db_path, 2, "Bob", "bob@example.com"),
insert_user(db_path, 3, "Charlie", "charlie@example.com")
)
# 查询用户
await query_users(db_path)
asyncio.run(main())
运行结果:
✅ 数据表创建成功
✅ 插入用户: Alice
✅ 插入用户: Bob
✅ 插入用户: Charlie
📋 用户列表:
ID: 1, 姓名: Alice, 邮箱: alice@example.com
ID: 2, 姓名: Bob, 邮箱: bob@example.com
ID: 3, 姓名: Charlie, 邮箱: charlie@example.com
四、常见注意事项
⚠️ 1. 不能在协程中使用同步阻塞库
python
# ❌ 错误示例:使用 requests(同步库)
import asyncio
import requests
async def bad_example():
response = requests.get("https://www.python.org") # 会阻塞事件循环!
return response.text
# ✅ 正确示例:使用 aiohttp(异步库)
import aiohttp
async def good_example():
async with aiohttp.ClientSession() as session:
async with session.get("https://www.python.org") as response:
return await response.text()
📌 常用异步库推荐:
- HTTP 请求:
aiohttp、httpx- 数据库:
aiosqlite、asyncpg(PostgreSQL)、aiomysql- Redis:
aioredis- 文件操作:
aiofiles
⚠️ 2. 避免在协程中执行 CPU 密集型任务
python
import asyncio
async def cpu_intensive():
# ❌ 这会阻塞事件循环
result = sum(i * i for i in range(10_000_000))
return result
# ✅ 正确做法:使用 run_in_executor 在线程池/进程池中执行
import concurrent.futures
def compute_sum(n):
"""独立函数,用于在进程池中执行"""
return sum(i * i for i in range(n))
async def cpu_intensive_correct():
loop = asyncio.get_event_loop()
# 对于 CPU 密集型任务,使用 ProcessPoolExecutor
# 注意:在 Windows 上需要 if __name__ == '__main__' 保护
with concurrent.futures.ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(
pool,
compute_sum,
10_000_000
)
return result
⚠️ 3. 正确处理异常
python
import asyncio
async def risky_task(task_id):
if task_id == 2:
raise ValueError(f"任务 {task_id} 出错了!")
await asyncio.sleep(1)
return f"任务 {task_id} 完成"
async def main():
tasks = [risky_task(i) for i in range(1, 4)]
# gather 默认会在第一个异常时停止
try:
results = await asyncio.gather(*tasks)
except ValueError as e:
print(f"捕获异常: {e}")
# 使用 return_exceptions=True 继续执行其他任务
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results, 1):
if isinstance(result, Exception):
print(f"任务 {i} 失败: {result}")
else:
print(f"任务 {i} 成功: {result}")
asyncio.run(main())
⚠️ 4. 协程超时控制
python
import asyncio
async def slow_task():
await asyncio.sleep(5)
return "完成"
async def main():
try:
# 设置 2 秒超时
result = await asyncio.wait_for(slow_task(), timeout=2.0)
except asyncio.TimeoutError:
print("任务超时!")
asyncio.run(main())
五、总结与延伸学习
🎯 核心要点回顾
- 协程 ≠ 多线程:协程是单线程内的任务切换,适合 I/O 密集型场景。
- 三大核心 :
async def定义协程,await等待结果,事件循环调度执行。 - 并发工具 :
asyncio.gather()批量执行,create_task()单独控制。 - 库的选择 :必须使用支持异步的库(如
aiohttp而非requests)。
📚 延伸学习建议
-
深入理解事件循环:
- 学习
asyncio.get_event_loop()的底层机制 - 了解
uvloop(更快的事件循环实现)
- 学习
-
异步上下文管理器:
pythonasync with aiohttp.ClientSession() as session: # 自动管理资源 -
异步生成器:
pythonasync def async_range(n): for i in range(n): await asyncio.sleep(0.1) yield i async for num in async_range(5): print(num) -
实战项目:
- 构建异步爬虫框架
- 开发高性能 Web API(FastAPI)
- 实现异步消息队列消费者
六、常见问题速查表
| 问题 | 解决方案 |
|---|---|
RuntimeError: asyncio.run() cannot be called from a running event loop |
在 Jupyter 中使用 await 而非 asyncio.run() |
| 协程没有执行 | 必须用 await 或 asyncio.run() 运行协程对象 |
TypeError: object async_generator can't be used in 'await' expression |
异步生成器需要用 async for 而非 await |
| 如何在同步代码中调用协程? | 使用 asyncio.run(coro()) 或 loop.run_until_complete(coro()) |
协程中如何使用 time.sleep()? |
必须用 await asyncio.sleep() 代替 |
| 如何取消正在运行的任务? | 使用 task.cancel() 并捕获 asyncio.CancelledError |
| 协程能提升 CPU 密集型任务性能吗? | 不能,应使用多进程(ProcessPoolExecutor) |
| 如何调试协程? | 启用调试模式:asyncio.run(main(), debug=True) |