协程与asyncio,异步编程入门:一篇搞定高并发!
pythonprint(f"开始请求 {url}")await asyncio.sleep(2) # 模拟网络延迟print(f"完成请求 {url}")return f"数据 from {url}"核心要点回顾1. 异步编程适合I/O密集型任务,能极大提升并发能力。2. 协程是可暂停的函数,async/await是它的语法糖。3. 事件循环是异步的引擎,负责调度任务。4. 用asyncio.cr
目录
一、为什么需要异步编程?
1.1 同步 vs 异步
想象一下,你中午点外卖:
同步模式:你下单后,就一直盯着手机看骑手到哪了,啥事也不干,直到外卖送到。这期间你浪费了大量时间在干等。
异步模式:你下单后,该打游戏打游戏,该写代码写代码。等外卖到了,门铃一响(通知),你再去取餐。
在程序世界里:
同步:一个任务没做完,CPU就一直等它,什么也不干。这是Python默认的执行方式。
异步:一个任务遇到耗时操作(比如读文件、请求网络),就主动让出CPU,让CPU去执行其他任务。等耗时操作完成了,再回来继续执行。
核心差异:异步不是并行,而是“遇到等待,主动切换”。它让单线程也能实现高并发。
1.2 阻塞 vs 非阻塞
阻塞:程序卡在那里,什么都不能做。比如time.sleep(5),这5秒内CPU完全被占着空转。
非阻塞:调用后立刻返回,不卡住。比如发起网络请求后,先去干别的,等数据回来了再处理。
异步编程的目标:把所有阻塞操作变成非阻塞,榨干CPU的每一分潜力。
1.3 多线程、多进程与协程的对比
多线程、多进程,它们和协程有什么区别?
| 方案 | 优点 | 缺点 | 适合场景 |
| 多线程 | 共享内存,切换开销较小 | 有全局锁(GIL),线程不安全,调试困难 | I/O密集型任务 |
| 多进程 | 真正并行,利用多核 | 资源占用大,进程间通信麻烦 | CPU密集型任务 |
| 协程 | 极低的切换开销,无锁,写法像同步代码 | 需要手动处理异步逻辑,不能利用多核 | 高并发I/O密集型 |
一句话总结:如果程序大部分时间在等待(网络请求、数据库查询、文件读写),协程是性价比最高的选择。
二、协程是什么?—— 可以“暂停”的函数
2.1 生成器与协程的渊源
在Python早期,协程是通过生成器(yield)实现的。生成器可以暂停执行,保存状态,下次再恢复。这正是协程需要的能力。
但用生成器写异步代码太反人类了,到处是yield和send。于是Python 3.5正式引入了async/await语法,让异步代码看起来就像普通的同步代码。
2.2 async/await 语法糖
async def:定义一个协程函数。调用它不会立即执行,而是返回一个协程对象。
await:等待一个可等待对象(另一个协程、Future或Task)。执行到这里,程序会挂起当前协程,去执行别的任务,直到等待完成再回来。
python
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1) # 模拟耗时操作,非阻塞等待
print("World")
# 协程函数不会自动运行,需要事件循环驱动
asyncio.run(say_hello())
注意:不能在普通函数里使用await,也不能在协程函数里使用time.sleep(那会真正阻塞整个线程),要用await asyncio.sleep。
三、asyncio 库实战 —— 让代码飞起来
3.1 事件循环(Event Loop)—— 管家
事件循环是异步编程的核心引擎。它就像一个永不停止的调度员,负责:
1. 注册任务(协程)
2. 当任务遇到await时,把它挂起,切换到另一个就绪的任务
3. 监测哪些任务等待的事件已完成,唤醒它们
4. 所有任务完成后,关闭循环
我们通常用asyncio.run(main())来创建、运行并关闭事件循环。
3.2 定义异步函数
python
async def fetch_data(url):
print(f"开始请求 {url}")
await asyncio.sleep(2) # 模拟网络延迟
print(f"完成请求 {url}")
return f"数据 from {url}"
3.3 运行异步任务(Task)
单个协程:asyncio.run(fetch_data("http://example.com"))
多个协程并发执行,需要把协程包装成Task。
python
async def main():
# 创建任务(同时开始执行)
task1 = asyncio.create_task(fetch_data("url1"))
task2 = asyncio.create_task(fetch_data("url2"))
# 等待所有任务完成
result1 = await task1
result2 = await task2
print(result1, result2)
asyncio.run(main())
# 输出:
开始请求 url1
开始请求 url2
完成请求 url1
完成请求 url2
数据 from url1 数据 from url2
注意两个请求几乎同时开始,总耗时只有2秒,而不是4秒!这就是并发的威力。
3.4 同时运行多个任务:gather & wait
asyncio.gather():并发执行多个任务,返回结果列表。
asyncio.wait():更灵活,可以设置超时、返回条件。
python
async def main():
tasks = [fetch_data("url1"), fetch_data("url2"), fetch_data("url3")]
results = await asyncio.gather(*tasks)
print(results) # 顺序对应任务顺序
asyncio.run(main())
3.5 超时与取消
用asyncio.wait_for给任务设置超时:
python
async def main():
try:
result = await asyncio.wait_for(fetch_data("slow_url"), timeout=1)
except asyncio.TimeoutError:
print("请求超时了!")
四、实战案例:异步爬取网页
我们来对比同步和异步爬虫的性能差距。假设要爬取10个网页。
4.1 同步版本(requests库)
python
import requests
import time
urls = ["https://httpbin.org/delay/1"] * 10 # 每个请求延迟1秒
def sync_fetch():
start = time.time()
for url in urls:
response = requests.get(url)
print(f"完成 {url}, 状态码 {response.status_code}")
print(f"同步耗时: {time.time() - start:.2f}秒")
sync_fetch() # 输出: 同步耗时: 约10秒
4.2 异步版本(aiohttp库)
python
import aiohttp
import asyncio
import time
async def fetch_async(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch_async(session, url) for url in urls]
await asyncio.gather(*tasks)
print(f"异步耗时: {time.time() - start:.2f}秒")
asyncio.run(main()) # 输出: 异步耗时: 约1秒
性能提升10倍!同步爬10个网页要等10秒,异步只用了1秒多(加上一点额外开销)。这还只是1秒延迟的例子,如果每个请求耗时5秒,差距会更夸张。
4.3 性能对比小结
同步:总时间 = 单个请求时间 × 请求数量
异步:总时间 ≈ 最慢的那个请求时间(并发足够大时)
注意:异步不是万能的。如果任务是CPU密集型的(比如计算圆周率),异步没用,这时用多进程。
五、常见坑点与注意事项
5.1 不能在异步函数中使用 time.sleep
time.sleep会阻塞整个线程,导致事件循环无法切换任务。一定要用await asyncio.sleep。
错误:
python
async def bad():
time.sleep(1) # 阻塞了!
正确:
python
async def good():
await asyncio.sleep(1)
5.2 注意线程安全问题
asyncio是单线程内的并发,通常不需要锁。但如果混用多线程(例如用run_in_executor),要小心共享数据的同步。
5.3 异步库的生态
很多传统库(如requests、openpyxl)不支持异步。需要使用对应的异步版本:
| 同步库 | 异步替代 |
| requests | aiohttp / httpx |
| sqlalchemy | databases + asyncpg / aiomysql |
| file open | aiofiles |
| redis | aioredis |
5.4 记住:协程函数必须被驱动
如果你定义了一个async def函数,但只是调用它而不await或asyncio.run,它会返回一个协程对象,什么都不会发生。
python
async def test():
print("不会打印")
test() # 只是得到一个协程对象,没有输出
六、总结
核心要点回顾
1. 异步编程适合I/O密集型任务,能极大提升并发能力。
2. 协程是可暂停的函数,async/await是它的语法糖。
3. 事件循环是异步的引擎,负责调度任务。
4. 用asyncio.create_task、gather、wait来并发执行多个任务。
5. 实战中替换阻塞库为异步版本,性能提升立竿见影。
如果觉得这篇文章对你有帮助,可以点赞、收藏、关注!
更多推荐


所有评论(0)