Python 异步编程入门:搞懂 async/await,这一篇就够了
做爬虫时发现同步请求要等好几秒,就想用异步来加快速度,结果被回调地狱和事件循环绕晕了。
问题是什么
我用 requests 写了个简单的爬虫,抓 10 个页面,串行执行花了 12 秒。
直觉告诉我应该「同时发请求」。但 Python 的 GIL 让多线程在 CPU 密集型任务上表现一般,多进程又太重。查了一圈发现,对于 I/O 密集型任务(网络请求、文件读写、数据库查询),异步编程才是正经解法。
结果打开 asyncio 文档那一刻——await、async def、event loop、coroutine、future、task……一堆概念扑面而来,代码写法也和正常的 Python 完全不一样。
解决思路
主流的 Python 并发方案有三条路:
| 方案 | 适用场景 | 心智负担 | 性能 |
|------|---------|---------|------|
| 多线程 (threading) | I/O 密集型 | 低,但有锁问题 | 一般(GIL 限制) |
| 多进程 (multiprocessing) | CPU 密集型 | 中,进程间通信麻烦 | 好,但开销大 |
| 异步 I/O (asyncio) | 高并发 I/O | 高(需要转变思维) | 最好,单线程搞定 |
我的场景是网络 I/O 密集型,选 asyncio 最合适。学习曲线是陡,但收益最大——单线程处理上万并发连接,同步代码做不到。
操作步骤
步骤1:理解核心概念——协程不是函数
异步编程的基础单元是协程(coroutine),不是函数。区别就两个关键字:
```python
# 这是一个普通函数
def hello():
return "Hello"
# 这是一个协程函数
async def hello_async():
return "Hello"
调用方式完全不同:
```python
# 普通函数直接调用
result = hello() # 返回 "Hello"
# 协程函数调用不会执行!返回一个 coroutine 对象
result = hello_async() # 返回 <coroutine object hello_async at 0x...>
# print(result) # 不会输出任何东西
**注意**: 这是初学者最容易踩的坑——`async def` 定义的函数,调用后不会立即执行,而是返回一个协程对象。得把它放到事件循环里才能跑。
要用 await 来执行协程:
```python
import asyncio
async def say_hello():
print("Hello...")
await asyncio.sleep(1) # 模拟 I/O 操作
print("...World!")
# await 必须在 async 函数里用
async def main():
await say_hello()
# 入口调用
asyncio.run(main())
await asyncio.sleep(1) 这行是精髓:碰到 await,当前协程就「挂起」,事件循环去跑别的协程,等 1 秒到了再回来继续。这就是异步的魔法。
📸[截图位置:控制台输出 "Hello... 等待1秒后 ...World!"]
步骤2:跑起第一个事件循环
事件循环是异步编程的调度中心,它负责:
1. 注册协程任务
2. 协程遇到 await 时挂起它
3. I/O 完成后唤醒对应的协程
4. 不断重复,直到所有任务完成
```python
import asyncio
async def task(name, delay):
print(f"任务 {name} 开始,需要 {delay} 秒")
await asyncio.sleep(delay)
print(f"任务 {name} 完成")
return f"{name} 的结果"
async def main():
# 顺序执行(和同步没区别,别这样写)
result1 = await task("A", 2)
result2 = await task("B", 1)
print(result1, result2)
asyncio.run(main()) # 总耗时 ~3秒
上面这是错的——await task("A", 2) 会等 2 秒,然后 await task("B", 1) 再等 1 秒,总共 3 秒,浪费了异步的能力。
要并发就得用 asyncio.gather 或 create_task:
```python
async def main():
# 并发执行!正确用法
task_a = asyncio.create_task(task("A", 2))
task_b = asyncio.create_task(task("B", 1))
# 注意:虽然 B 后启动,但 B 先完成(只需要 1 秒)
result_a = await task_a
result_b = await task_b
print(result_a, result_b)
asyncio.run(main()) # 总耗时 ~2秒(取最长任务)
看到了吗?create_task 把协程包装成 Task,提交到事件循环后就立即开始执行,后面的 await 只是等结果。两个任务同时在跑了。
📸[截图位置:输出对比——顺序执行 vs 并发执行的时间差异]
步骤3:用 `asyncio.gather` 批量跑任务
create_task 适合任务数量固定的场景。如果有一堆任务要跑,用 gather 更优雅:
```python
import asyncio
async def fetch_url(url):
print(f"开始请求: {url}")
await asyncio.sleep(1) # 模拟网络请求
return f"数据来自 {url}"
async def main():
urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments",
"https://api.example.com/tags",
"https://api.example.com/categories",
]
# 并发请求所有 URL
results = await asyncio.gather(
*(fetch_url(url) for url in urls)
)
for result in results:
print(f"收到: {result}")
asyncio.run(main()) # 总耗时 ~1秒,不是 5 秒!
**注意**: `gather` 返回的结果顺序和传入的任务顺序一致,和任务完成先后无关。上面虽然 5 个请求同时发出,但 `results` 列表的顺序就是 urls 的顺序。
📸[截图位置:5个请求同时发出,1秒后全部完成]
步骤4:实战——用 aiohttp 写真正的异步爬虫
前面用 asyncio.sleep 模拟,现在上真家伙。aiohttp 是 Python 最流行的异步 HTTP 库:
```bash
pip install aiohttp
```python
import asyncio
import aiohttp
import time
async def fetch_one(session, url):
async with session.get(url) as response:
# 注意:response.text() 也是异步的
html = await response.text()
return len(html) # 返回页面大小
async def main():
urls = [
"https://httpbin.org/delay/1", # 会等 1 秒返回
"https://httpbin.org/delay/2", # 会等 2 秒返回
"https://httpbin.org/delay/3", # 会等 3 秒返回
]
start = time.time()
# 异步方式:并发请求
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
sizes = await asyncio.gather(*tasks)
end = time.time()
print(f"页面大小: {sizes}")
print(f"异步耗时: {end - start:.2f} 秒") # ~3秒(最长那个)
# 对比:同步方式
import requests
start = time.time()
for url in urls:
resp = requests.get(url)
print(f"同步: {len(resp.text)}")
end = time.time()
print(f"同步耗时: {end - start:.2f} 秒") # ~6秒(1+2+3)
asyncio.run(main())
输出示例:
```
页面大小: [314, 542, 768]
异步耗时: 3.02 秒
同步: 314
同步: 542
同步: 768
同步耗时: 6.15 秒
异步比同步快了一倍!URL 越多,差距越明显。100 个请求每个耗时 1 秒,同步要 100 秒,异步只需要 1 秒出头。
📸[截图位置:异步 vs 同步的耗时对比输出]
步骤5:踩坑记录——常见的 3 个坑
坑 1:在同步代码里调异步
```python
import asyncio
async def fetch_data():
await asyncio.sleep(1)
return "data"
# ❌ 这行会报错
result = fetch_data()
# RuntimeWarning: coroutine 'fetch_data' was never awaited
# ✅ 正确做法
result = asyncio.run(fetch_data())
坑 2:在异步代码里用同步阻塞库
```python
import asyncio
import requests # 同步库!
async def fetch():
# ❌ requests.get 是同步阻塞的,会卡住整个事件循环
resp = requests.get("https://httpbin.org/get")
return resp.text
async def main():
tasks = [fetch() for _ in range(10)]
# 这并没有并发!因为 requests.get 阻塞时事件循环无法切换
results = await asyncio.gather(*tasks)
得用异步版本的库:aiohttp 替代 requests,aiomysql 替代 pymysql,asyncpg 替代 psycopg2。
坑 3:忘了加超时
```python
import asyncio
# ❌ 没有超时,请求挂了协程就卡死
async def fetch_without_timeout():
# 如果这个请求一直不返回…
await some_slow_request()
# ✅ 加超时
async def fetch_with_timeout():
try:
async with asyncio.timeout(5): # Python 3.11+
await some_slow_request()
except asyncio.TimeoutError:
print("请求超时了!")
**注意**: Python 3.11 引入的 `asyncio.timeout()` 是上下文管理器用法。3.10 及以下版本用 `asyncio.wait_for()`。
📸[截图位置:超时触发的 TimeoutError 异常信息]
结果与总结
花了一个周末搞懂 Python 异步编程后,我的爬虫从 12 秒降到了 1.2 秒,10 倍的提升。核心收获:
最大的坑是思维模式切换——不能用写同步代码的脑子去写异步代码。习惯了以后,你会发现 await 其实就是一个「让出控制权」的信号:「我现在要等 I/O,你先去执行别的任务,准备好了叫我。」
延伸思考
截图清单
1. 📸 控制台输出 Hello... 等待 1 秒后 ...World! 的演示结果
2. 📸 顺序执行 vs 并发执行的时间对比输出
3. 📸 5 个 aiohttp 请求并发发出、1 秒后全部完成的输出
4. 📸 异步(~3 秒)vs 同步(~6 秒)的耗时对比
5. 📸 超时场景下 asyncio.TimeoutError 异常信息

评论已关闭!