ThreadPoolExecutor比requests.get()串行快,因其通过多线程重叠I/O等待时间(如DNS、TCP握手),理论耗时趋近于最长单请求RTT;但需配合Session复用连接、合理设线程数、封装任务函数并用future.result(timeout=)捕获超时与网络异常。

为什么ThreadPoolExecutor比requests.get()串行快?
因为爬虫大部分时间卡在等待响应(DNS解析、TCP握手、服务器处理),而不是CPU计算。线程池能同时发起多个请求,把“等”的时间重叠起来。单线程串行发10个请求,总耗时≈10个RTT之和;用4线程,理论下限接近最长那个请求的RTT。
但要注意:ThreadPoolExecutor不解决DNS阻塞或连接复用问题——它只是并发调度,底层仍依赖requests的会话管理。没配Session的话,每个requests.get()都新建TCP连接,反而可能拖慢整体速度。
- 务必配合
requests.Session()复用连接和Cookie - 线程数不是越多越好,一般设为
min(32, CPU核心数 * 5),超过容易触发目标站反爬或本地端口耗尽 - 别在
submit()里直接传requests.get(url),要封装成函数,方便传参和异常捕获
如何正确提交任务并捕获超时/连接错误?
直接submit(task, url)后不处理返回值,出错就静默失败。必须用future.result(timeout=)显式取结果,并包住TimeoutError、ConnectionError、HTTPError等。
常见错误是把timeout只设在requests.get()里,却忘了future.result()本身也有超时——如果某个请求卡死(比如服务器挂了但没发FIN),result()会一直等下去,拖垮整个线程池。
立即学习“Python免费学习笔记(深入)”;
-
requests.get(url, timeout=(3, 7)):3秒连不上,7秒收不到响应体 -
future.result(timeout=10):最多等10秒拿到这个future的结果,超时抛concurrent.futures.TimeoutError - 捕获
requests.exceptions.RequestException覆盖多数网络异常
ThreadPoolExecutor和aiohttp+asyncio哪个更适合爬虫?
纯I/O密集型场景下,aiohttp通常吞吐更高、内存更省,但调试难度大——异常栈深、断点难打、第三方库支持弱(比如lxml不能直接await)。而ThreadPoolExecutor逻辑直白,可直接复用已有同步代码,适合快速上线或混合业务(比如爬完立刻用pandas处理)。
真正卡点在于:如果你的爬虫要跑几千个URL,且目标站响应稳定,aiohttp能压到100+并发;但若URL质量差(大量404/重定向/反爬页),ThreadPoolExecutor配合重试策略反而更稳——线程崩溃不影响其他任务,而asyncio里一个未处理异常可能让整个event loop停摆。
- 新手/小规模/需集成同步库 → 选
ThreadPoolExecutor - 高并发/低延迟/长期运行服务 → 优先评估
aiohttp - 别混用:不要在
submit()里调async def函数,会报RuntimeWarning: coroutine was never awaited
如何避免线程池阻塞主线程或资源泄漏?
ThreadPoolExecutor默认不自动关闭,如果忘记调shutdown(wait=True),程序可能不退出;如果设wait=False又可能丢掉还没完成的任务。最安全的是用with语句上下文管理。
另一个坑是全局共享Session:多线程共用一个requests.Session()对象是线程安全的,但如果你在任务函数里自己session = requests.Session(),那就每线程一个新实例,连接池失效,还可能触发ResourceWarning: unclosed <ssl.sslsocket></ssl.sslsocket>。
- 用
with ThreadPoolExecutor(max_workers=4) as executor:确保自动清理 -
Session对象应在with块外创建,传入任务函数,而非在每个任务里重建 - 若需自定义DNS或代理,通过
Session.mount()配HTTPAdapter,别在每次请求里重复设置
真正麻烦的从来不是并发本身,而是怎么让每个请求既快又稳——连接复用、超时分层、错误隔离、资源回收,这些细节漏掉任何一环,线程数加得再高也白搭。


















