Python的异步把我坑惨了,原来async/await和多线程的区别这么大

简介: 这是一个真实爬虫项目复盘:同步耗4小时,多线程易被封IP,最终用asyncio+aiohttp异步方案,单线程10分钟搞定万级请求,性能提升10–100倍,资源占用低、并发可控。(239字)

一个让我怀疑人生的爬虫项目

前年接了一个数据采集项目。每天要爬一万多个商品页面的价格和库存信息,每个页面请求大概等1秒钟。

我心想:“一万个请求,每个一秒,也就两个多小时嘛,跑着呗。”

结果跑起来发现根本不是这么回事。同步请求一个接一个,加上网络波动和重试,跑完一轮要四个多小时。客户那边每天等着数据更新,四个多小时黄花菜都凉了。

我说:“上多线程!”

开了50个线程并发请求,速度确实快了,跑完一轮大概十几分钟。但新问题来了——线程开太多,服务器连接数爆了,被目标网站封了IP。降低到20个线程,速度又慢回去了。

同事看了一眼我的代码:“你这个场景,用异步啊。”

我:“异步?async/await那套?跟多线程有什么区别?”

那天下午,我把async/await和多线程的区别彻底研究了一遍。今天把这些东西讲清楚,希望你下次遇到类似问题别像我一样纠结。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (49).png

第一步:先搞清楚并发到底在解决什么问题

不管是多线程还是异步,解决的都是同一个问题:别让程序傻等着

看一段最朴素的同步代码:

import time
import requests

def fetch(url):
   print(f"开始请求 {url}")
   response = requests.get(url)  # 这里傻等着
   print(f"完成请求 {url}")
   return response

start = time.time()
fetch("网站A")
fetch("网站B")
fetch("网站C")
print(f"总耗时: {time.time() - start}")

三个请求,每个等1秒,总共3秒。

问题出在哪?requests.get(url)这行代码执行的时候,程序啥事也没干,就在那儿干等着网络返回数据。CPU其实是空闲的,但同步代码不允许它去做别的事——没执行完当前函数,绝不跳到下一行。

并发要解决的就是这个“等待浪费”。当一个操作需要等待时,让程序暂时离开,去做别的事,等那个操作准备好了再回来继续。

第二步:多线程——增加人手

多线程的思路很直接:既然一个人等的时候干不了活,那就多招几个人

每个线程就像一个独立的员工。员工A在等网络响应的时候,员工B可以继续发请求,员工C可以继续发请求。这样CPU就不会闲着了。

import threading
import requests

def fetch(url):
   response = requests.get(url)
   print(f"完成 {url}")

urls = ["网站A", "网站B", "网站C"]
threads = []
for url in urls:
   t = threading.Thread(target=fetch, args=(url,))
   t.start()
   threads.append(t)
for t in threads:
   t.join()

三个线程同时干活,总耗时约1秒。

但多线程有个代价:线程切换由操作系统控制。操作系统会时不时打断当前线程,切换到另一个线程。这种切换叫做“抢占式”——你控制不了什么时候切、切给谁。

就像开一个多窗口的银行,每个窗口一个柜员。如果某个客户填表填得慢,那个窗口就得等着,但其他窗口还能继续服务别人。问题是柜员数量有限,招太多人成本也高——线程太多,操作系统切换开销大,还可能把服务器资源耗尽。

第三步:异步——一个超级柜员

异步的思路完全不同:不增加人手,而是让一个人变得更高效

import asyncio
import aiohttp

async def fetch(session, url):
   print(f"开始请求 {url}")
   async with session.get(url) as response:
       data = await response.text()  # 主动让出控制权
   print(f"完成请求 {url}")
   return data

async def main():
   async with aiohttp.ClientSession() as session:
       tasks = [
           fetch(session, "网站A"),
           fetch(session, "网站B"),
           fetch(session, "网站C"),
       ]
       await asyncio.gather(*tasks)

asyncio.run(main())

异步的核心是一个叫做 “事件循环” 的东西。你可以把它理解成一个超级调度员,管理着所有待执行的任务。

当一个异步任务遇到需要等待的操作时(比如网络请求),它不会傻等,而是主动告诉事件循环:“我要等一会儿,你先去忙别的,好了叫我”。事件循环收到消息后,立刻切换到下一个可以执行的任务。

等网络数据回来了,事件循环再把之前暂停的任务唤醒,继续执行。

用银行的比喻:异步就像一个超级王牌柜员,只开一个窗口。客户A来办业务,发现需要回去拿身份证。这个柜员不会傻等,而是立刻跟客户A说:“你先去拿,拿好了再回来,我先给下一个人办。”然后马上开始处理客户B的业务。

等客户A拿着身份证回来了,柜员再找个空隙把剩下的业务办完。这个柜员从来没有闲着,通过在任务的等待时间里来回切换,一个人就搞定了多个客户的活儿。

关键区别在于:多线程是操作系统帮你切换(抢占式),你控制不了;异步是任务自己主动让出控制权(协作式),你自己说了算。

第四步:到底有什么区别?

说了这么多,用一张表总结一下:

维度 多线程 async/await
调度者 操作系统(抢占式) 事件循环(协作式)
资源消耗 每个线程约1-8MB内存 每个任务几乎不占额外内存
并发上限 几十到几百个线程 几千甚至上万个任务
切换开销 操作系统上下文切换,较重 函数调用级别,很轻
适用场景 I/O密集型,少量并发 I/O密集型,海量并发
CPU密集型 受GIL限制,基本没用 没用(单线程)
学习曲线 相对平缓 相对陡峭,需要理解事件循环

用我那个爬虫项目来实测:同步版本4个多小时,20线程版本约20分钟但容易被封,异步版本用aiohttp跑一万个请求,不到10分钟跑完,而且单线程连接数可控,不容易被封。

有实测数据显示,在4核8G服务器上,aiohttp可以维持3000+并发连接,而传统的同步方式超过500连接就会出现性能断崖式下跌。异步爬虫比同步快10到100倍

第五步:还有一些坑

异步虽然强大,但坑也不少。我踩过的几个分享给你。

坑一:逐个await等于没用异步

这是最常见也最隐蔽的错误。

# 错误写法——还是串行
async def main():
   await fetch("网站A")  # 等A完成
   await fetch("网站B")  # 再等B完成
   await fetch("网站C")  # 再等C完成

这样写跟同步没区别,总耗时还是3秒。正确做法是用asyncio.gather()并发执行:

# 正确写法——真正并发
async def main():
   await asyncio.gather(
       fetch("网站A"),
       fetch("网站B"),
       fetch("网站C"),
   )

坑二:在异步里调用同步阻塞函数

事件循环是单线程的。如果你在协程里调用了time.sleep()或者同步的requests.get(),整个事件循环都会被卡住。

async def bad_example():
   time.sleep(5)  # 卡死整个事件循环5秒!
   # 应该用 await asyncio.sleep(5)

如果是必须用的同步库,可以用asyncio.to_thread()把它丢到线程池里执行。

坑三:忘了await

async def main():
   fetch("网站A")  # 忘了await!这行什么都没干
   await fetch("网站B")

不写await,协程根本不会执行。Python 3.11+会给出警告,但最好还是养成习惯——写了async def,调用的时候一定加await

总结

一句话记住区别:多线程是操作系统帮你切换任务,异步是你自己主动让出控制权

用餐厅来总结:

  • 多线程:招了10个厨师,每个厨师负责一道菜。等菜的时候厨师闲着,但其他厨师还在忙
  • 异步:只有1个超级厨师,但他从来不等——菜在锅里炖的时候,他立刻去切下一道菜的食材

现在我遇到并发需求,会先问自己两个问题:

  1. 任务是等(I/O)还是算(CPU)?等的话才考虑并发
  2. 并发量多大?几十个用多线程,几百上千个用异步

想清楚再动手,少让程序卡住几次。

目录
相关文章
|
21天前
|
数据采集 人工智能 自然语言处理
自动化比价系统:从采集到数据清洗,全链路打通教程
本文详解淘宝/京东/拼多多三平台自动化比价系统全链路:用OpenClaw自然语言采集、站大爷隧道代理防封(24小时成功率98.2%+)、AI智能清洗价格(统一格式、核销优惠、去重校验),自动生成可决策的比价报告与飞书预警,真正实现“采得稳、洗得准、用得上”。
154 1
|
21天前
|
数据采集 机器学习/深度学习 Java
Python 的多线程就是个摆设?不,原来是你没选对IO场景,这差距大到离谱
本文深入剖析Python多线程性能之谜:揭秘GIL机制如何限制CPU密集型任务的并行效率,同时阐明其在IO密集型场景(如爬虫、文件读写)中的显著优势。通过实测对比,清晰指出——CPU型任务选多进程,IO型任务用多线程,并附实战选型指南与Python 3.13自由线程前瞻。
103 0
|
25天前
|
JSON 监控 大数据
Python的生成器把我坑惨了,原来yield和return的区别这么大
本文以一次日志分析导致服务器内存爆表的实战经历为引,深入浅出对比 Python 中 `return` 与 `yield` 的本质区别:前者“一次性交作业”,后者“边做边上菜”。通过食堂打饭等生动比喻,详解生成器的内存优势、使用场景及常见陷阱,助你高效处理大数据。
120 0
|
云安全 SQL 弹性计算
阿里云提示网站后门发现后门(Webshell)文件的解决办法
2018年10月27日接到新客户网站服务器被上传了webshell脚本木马后门问题的求助,对此我们sine安全公司针对此阿里云提示的安全问题进行了详细分析,ECS服务器被阿里云提示异常网络连接-可疑WebShell通信行为,还会伴有,网站后门-发现后门(Webshell)文件,以及提示网站后门-一句话webshell的安全提示,但是大部分都是单独服务器ECS的用户,具体被阿里云提示的截图如下:
阿里云提示网站后门发现后门(Webshell)文件的解决办法
|
4月前
|
消息中间件 运维 安全
非得显卡?小模型跑在CPU上也照样快
Aether项目聚焦边缘/无GPU/私有化场景,用≤9B小模型构建高可用智能运维Agent:融合RAG知识库、分级意图路由、SOP式Skill编排与LoRA微调,兼顾数据安全、低资源消耗与强领域专业性。(238字)
743 2
|
5月前
|
存储 数据采集 人工智能
|
测试技术 Python
将秒换算成时、分、秒
本文介绍了使用Python将总秒数转换为小时、分钟和秒的格式的方法,包括定义转换函数和格式化输出函数,并提供了完整的代码实现及测试用例,帮助用户更友好地展示时间信息。
1129 59
|
机器学习/深度学习 存储 人工智能
算法金 | LSTM 原作者带队,一个强大的算法模型杀回来了
**摘要:** 本文介绍了LSTM(长短期记忆网络)的发展背景和重要性,以及其创始人Sepp Hochreiter新推出的xLSTM。LSTM是为解决传统RNN长期依赖问题而设计的,广泛应用于NLP和时间序列预测。文章详细阐述了LSTM的基本概念、核心原理、实现方法和实际应用案例,包括文本生成和时间序列预测。此外,还讨论了LSTM与Transformer的竞争格局。最后,鼓励读者深入学习和探索AI领域。
617 7
算法金 | LSTM 原作者带队,一个强大的算法模型杀回来了
|
存储 域名解析 Kubernetes
从零开始,在 Kubernetes 上玩转 Erda(二)
本章节介绍 Erda 的部署以及配置细节
2348 0
从零开始,在 Kubernetes 上玩转 Erda(二)