来源:网络
做后端开发最怕遇到什么?我问过身边不少程序员,答案出奇一致——异步代码。
同步代码写久了,一上来就看到 async def、await,脑子里全是问号。今天这篇文章,我用最直白的语言,把 Python 异步编程的核心逻辑讲清楚。
为什么需要异步?
先说一个真实场景。
你写了一个爬虫,要从10个网站抓数据。同步写法是这样的:
import requestsdeffetch_all(): results = []for url in urls: resp = requests.get(url) # 每个请求阻塞等待 results.append(resp.json())return results
假设每个请求平均耗时1秒,10个网站就要10秒。但如果这10个请求可以同时发出,理论上1秒就能搞定。
异步编程解决的就是这个问题——在等待 I/O(网络请求、文件读写、数据库查询)的时候,让 CPU 去干别的事。
三个核心概念
1. async / await:协程的语法糖
Python 3.5 引入了这两个关键字,这是现代异步代码的标准写法:
import asyncioasyncdefhello():print("Hello")await asyncio.sleep(1) # 暂停1秒,但不阻塞其他任务print("World")# 运行协程asyncio.run(hello())
async def 定义一个协程函数,调用它不会立即执行,而是返回一个协程对象。await 用来等待另一个协程完成,期间把控制权交出去,让其他任务有机会运行。
2. asyncio:事件循环
asyncio 是 Python 内置的异步库,核心是一个事件循环(Event Loop)。
你可以把它理解成一个"调度员",它同时管理多个协程的运行状态。当某个协程在 await 时让出 CPU,调度员立刻切换到另一个就绪的协程继续执行。
import asyncioasyncdeftask(name, seconds):print(f"{name} 开始,等待 {seconds} 秒")await asyncio.sleep(seconds)print(f"{name} 完成")asyncdefmain():# 创建两个任务,同时执行await asyncio.gather( task("任务A", 2), task("任务B", 1), )asyncio.run(main())# 输出:# 任务A 开始,等待 2 秒# 任务B 开始,等待 1 秒# 任务B 完成# 任务A 完成
注意看执行顺序:任务A和任务B同时启动,B只等1秒所以先完成。整个过程只用了2秒,而不是3秒。
3. 协程 vs 线程:谁更快?
有人会说,"多线程不也能并发吗?"
能。但区别很大:
线程虽好,但创建成本高、切换开销大。当你的程序大量时间花在等待网络响应时,协程是更优雅的选择。
实战:用 aiohttp 写异步爬虫
光说不练假把式,来一个真实可用的例子。
同步爬虫(10个网站串行请求):
import requestsimport timedefsync_crawl(urls): start = time.time() results = []for url in urls: r = requests.get(url, timeout=5) results.append(r.json())print(f"同步耗时:{time.time() - start:.2f}s")return results
异步爬虫:
import aiohttpimport asyncioimport timeasyncdefasync_crawl(urls): start = time.time()asyncwith aiohttp.ClientSession() as session:asyncdeffetch(url):asyncwith session.get(url) as resp:returnawait resp.json() results = await asyncio.gather(*[fetch(url) for url in urls])print(f"异步耗时:{time.time() - start:.2f}s")return results
实际测试中,10个网站:
性能差距立竿见影。
常见坑:不要在异步里调同步代码
这是新手最容易踩的坑。
asyncdefbad_example():# ❌ 千万别这样写!import pandas as pd df = pd.read_csv("data.csv") # pandas 是同步库,会阻塞整个事件循环return dfasyncdefgood_example():# ✅ 用 asyncio.to_thread 把同步操作放到线程池import pandas as pddefread_csv():return pd.read_csv("data.csv") df = await asyncio.to_thread(read_csv)return df
原则只有一条:异步代码里,只用异步库。遇到必须用同步库的场景,用 asyncio.to_thread() 包装一下,让它在后台线程里执行。
异步生态:这些库你迟早会用到
如果你正在用 FastAPI 开发 API,服务默认就是异步的,搭配这些异步库使用,性能直接拉满。
写在最后
异步编程的本质,是让等待的时间被充分利用。一旦理解了这个逻辑,再看 async/await 就不会觉得神秘了。
我个人的经验是:先把 asyncio.gather 用熟,能并发执行多个协程就已经解决了80%的场景。等真正遇到复杂任务调度时,再去深入研究信号量、队列、任务组这些进阶用法。
学编程没有捷径,但有正确的方法。动手写一个异步爬虫,跑出结果的那一刻,你会和我一样觉得——原来异步也就这么回事。
长按或扫描下方二维码,免费获取 Python公开课和大佬打包整理的几百G的学习资料,内容包含但不限于Python电子书、教程、项目接单、源码等等
▲扫描二维码-免费领取
推荐阅读
8个Python高效数据分析的技巧!(附完整代码)
掌握这20个 SQL 优化方法,你就无敌了!
10个强大的Python数据科学技巧
太香了!Python 优化提速的 8 个小技巧!