
写代码的人最怕什么?页面卡,用户骂,老板问。
但有时候真不是代码逻辑有问题,是你的程序在同步执行——一个任务等另一个任务,像极了食堂排队打饭,前面的人慢,后面全得等。
今天聊聊Python异步编程,帮你把这事一次搞明白。
往期阅读>>>
Python 自动化管理Jenkins的15个实用脚本,提升效率
App2Docker:如何无需编写Dockerfile也可以创建容器镜像
Python 自动化识别Nginx配置并导出为excel文件,提升Nginx管理效率
同步代码长这样:
importrequestsdefget_data():r1 = requests.get('https://api.example.com/user/1')r2 = requests.get('https://api.example.com/user/2')r3 = requests.get('https://api.example.com/user/3')return [r1.json(), r2.json(), r3.json()]
三个请求,一个接一个执行。总耗时 = 请求1 + 请求2 + 请求3。如果每个请求200ms,光等待就要600ms。
异步代码是这样:
importasyncioimporthttpxasyncdefget_user(client, user_id):response = awaitclient.get(f'https://api.example.com/user/{user_id}')returnresponse.json()asyncdefmain():asyncwithhttpx.AsyncClient() asclient:tasks = [get_user(client, i) foriinrange(1, 4)]results = awaitasyncio.gather(*tasks)returnresults
三个请求同时发出,总耗时 ≈ 最慢那个请求的时间。200ms搞定。
差距就这么大。
上手异步之前,先把这三个东西搞清楚。
async 声明这个函数会“让位”——执行到这,CPU可以先去干别的。
asyncdeffetch_data(): ...
await 就是“等一等”,等另一个异步操作的结果。
result = awaitsome_async_function()asyncio 是Python内置的异步库,提供了事件循环这些底层能力。你基本不需要直接操作它,但得 import 它:
importasyncio就这么简单,别想复杂了。
光说不练假把式,上个真实场景。
我要抓取GitHub trending页面100个仓库的信息,同步写法:
importrequestsimporttimedeffetch_repos():urls = [f'https://api.github.com/repos/{i}'foriinrange(100)]results = []forurlinurls:r = requests.get(url)results.append(r.json())returnresultsstart = time.time()fetch_repos()print(f'同步耗时: {time.time() - start:.2f}s')
在我这台机器上,跑了 48秒。
换成异步:
importasyncioimporthttpximporttimeasyncdeffetch_one(client, repo_id):url = f'https://api.github.com/repos/test/repo{repo_id}'r = awaitclient.get(url)returnr.json()asyncdefmain():asyncwithhttpx.AsyncClient() asclient:tasks = [fetch_one(client, i) foriinrange(100)]results = awaitasyncio.gather(*tasks)returnresultsstart = time.time()asyncio.run(main())print(f'异步耗时: {time.time() - start:.2f}s')
异步版本只用了 3.2秒。
提速 15倍,代码就多了4行。这事你自己品。
异步虽好,但有几个坑不踩不知道。
坑一:用了异步库,但函数没声明async
# 报错requests.get('https://example.com')# 正确asyncwithhttpx.AsyncClient() asclient:awaitclient.get('https://example.com')
记住:只要涉及IO操作的地方,前面都得加 await,函数前面都得加 async。
坑二:在异步函数里调用同步代码
asyncdefbad_example():# 不要这样!time.sleep(5) # 这会阻塞整个事件循环asyncdefgood_example():# 正确做法awaitasyncio.sleep(5) # 异步sleep,不会阻塞其他任务
同步的 time.sleep() 会把整个程序卡住,异步版本的 asyncio.sleep() 才会交出执行权。
坑三:忘记 gather 了
asyncdefwrong_way():# 顺序执行,没有并发r1 = awaitfetch(url1)r2 = awaitfetch(url2)asyncdefright_way():# 并发执行r1, r2 = awaitasyncio.gather(fetch(url1), fetch(url2))
如果你一个个 await,跟同步写法没区别。并发要靠 asyncio.gather() 或 asyncio.create_task() 来实现。
坑四:并发数没控制,直接被封IP
异步执行太快了,100个请求可能1秒内全发出去。服务器觉得你是在攻击,直接拉黑。
解决方案:加信号量控制并发数。
importasyncioimporthttpxasyncdeffetch_with_limit(semaphore, client, url):asyncwithsemaphore:returnawaitclient.get(url)asyncdefmain():semaphore = asyncio.Semaphore(5) # 最多同时5个请求asyncwithhttpx.AsyncClient() asclient:tasks = [fetch_with_limit(semaphore, client, f'https://example.com/{i}')foriinrange(100)]awaitasyncio.gather(*tasks)
加个信号量,想快就快,想慢就慢。
异步不是万能药,用错了反而麻烦。
适合异步的场景:
网络请求(爬虫、API调用)
文件读写(特别是多个文件同时操作)
数据库查询(连接池 + 异步驱动)
Web服务(FastAPI、Sanic 这类框架)
不适合异步的场景:
CPU密集型任务(图片处理、机器学习、加密计算)
简单的一次性操作
你不熟悉的代码(异步debug比同步难多了)
一个判断标准:如果你的瓶颈在IO等待,而不是CPU计算,异步大概率能帮到你。
今天聊了几个核心点:
同步代码排队执行,异步代码“叫号”并发
三个关键字打天下:async、await、asyncio
并发靠 gather,限流靠 Semaphore
同步库(requests、time)不能直接用,要找异步替代品
异步不适合CPU密集型任务
异步编程这东西,上手成本不高,但理解透需要时间。建议你找个小项目练练手,比如把之前的同步爬虫改一版。改完你会发现:原来卡你半天的那些操作,根本不是代码问题,是写法问题。
你用异步编程踩过什么坑?或者你现在有什么场景想用异步但不知道怎么写?评论区聊聊,我来帮你看看。
觉得有用就转发给身边写Python的朋友,点在看,下次聊点更硬核的。
https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c
