
写代码的人最怕什么?页面卡,用户骂,老板问。
但有时候真不是代码逻辑有问题,是你的程序在同步执行——一个任务等另一个任务,像极了食堂排队打饭,前面的人慢,后面全得等。
今天聊聊Python异步编程,帮你把这事一次搞明白。
同步代码长这样:
importrequests
defget_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。
异步代码是这样:
importasyncio
importhttpx
asyncdefget_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个仓库的信息,同步写法:
importrequests
importtime
deffetch_repos():
urls = [f'https://api.github.com/repos/{i}'foriinrange(100)]
results = []
forurlinurls:
r = requests.get(url)
results.append(r.json())
returnresults
start = time.time()
fetch_repos()
print(f'同步耗时: {time.time() - start:.2f}s')在我这台机器上,跑了 48秒。
换成异步:
importasyncio
importhttpx
importtime
asyncdeffetch_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)
returnresults
start = 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秒内全发出去。服务器觉得你是在攻击,直接拉黑。
解决方案:加信号量控制并发数。
importasyncio
importhttpx
asyncdeffetch_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)加个信号量,想快就快,想慢就慢。
异步不是万能药,用错了反而麻烦。
适合异步的场景:
不适合异步的场景:
一个判断标准:如果你的瓶颈在IO等待,而不是CPU计算,异步大概率能帮到你。
今天聊了几个核心点:
异步编程这东西,上手成本不高,但理解透需要时间。建议你找个小项目练练手,比如把之前的同步爬虫改一版。改完你会发现:原来卡你半天的那些操作,根本不是代码问题,是写法问题。
你用异步编程踩过什么坑?或者你现在有什么场景想用异步但不知道怎么写?评论区聊聊,我来帮你看看。
觉得有用就转发给身边写Python的朋友,点在看,下次聊点更硬核的。
“无他,惟手熟尔”!有需要的用起来!
本文分享自 Nicholas与Pypi 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!