max_workers控制线程池中同时存活的最大线程数,而非瞬时执行任务数;设为5时,100个请求最多5线程并发,其余排队,其默认值通常为min(32, os.cpu_count() + 4)。

ThreadPoolExecutor 的 max_workers 参数到底控制什么
它控制的是线程池中**同时存活的最大线程数**,不是“同一时刻正在执行的任务数”——因为线程可能阻塞在 I/O(比如 requests.get()),但依然算作“存活”。所以设 max_workers=5,爬虫发 100 个请求时,最多有 5 个线程在跑,其余任务排队等待空闲线程。
常见误解是以为它能限制 TCP 连接数或 HTTP 并发请求数,其实不能。真正影响网络并发的还有:requests.Session 的连接池、DNS 缓存、系统文件描述符限制。
- 若不指定
max_workers,默认值通常是min(32, os.cpu_count() + 4),对 I/O 密集型爬虫明显偏高,容易触发反爬或连接拒绝 - 实际建议从
3–10开始试,配合目标网站响应时间与稳定性调整 - 设得过小(如
1)会退化成串行,失去并发意义;过大(如50)可能被封 IP 或遭遇ConnectionError: Max retries exceeded
requests + ThreadPoolExecutor 组合时必须加 session 复用
每次新建 requests.get() 都会创建新连接,即使用了线程池,也极易耗尽本地端口或触发服务器限流。正确做法是在每个线程里复用一个 Session 实例,或更稳妥地——在 ThreadPoolExecutor 外创建单个 Session,并传入任务函数。
from concurrent.futures import ThreadPoolExecutor
import requests
<p>session = requests.Session()</p><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/00968c3c2c15" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">Python免费学习笔记(深入)</a>”;</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/skill11111" title="python-flask-vue-creator"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20e695c2374.jpg" alt="python-flask-vue-creator" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/skill11111" title="python-flask-vue-creator">python-flask-vue-creator</a>
<p>创建 Flask + Vue.js 全栈项目,采用单端口架构、uv 依赖管理、结构化日志和生产级前端设计。当用户要求以下操作时触发:创建新的 Flask+Vue 项目、搭建 Web 应用、构建全栈 Python+Vue 应用、初始化 Flask Web 项目、设置单端口 Flask 前后端项目。触发词:新建项目、创建项目、搭建项目、Flask项目、Vue项目。</p>
</div>
<a href="/xiazai/skill11111" title="python-flask-vue-creator" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><h1>复用连接池,避免重复握手</h1><p>session.headers.update({'User-Agent': 'Mozilla/5.0'})</p><p>def fetch_url(url):
try:
resp = session.get(url, timeout=10)
return resp.status_code, len(resp.content)
except Exception as e:
return None, str(e)</p><p>urls = ['<a href="https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c">https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c</a>'] * 20
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(fetch_url, urls))
- 不要在
fetch_url内部新建Session,否则连接池失效,max_workers就只是“并发开浏览器”的假象 - 如果需要不同 UA 或 cookie,可考虑用
threading.local()绑定每线程一个 session,但多数场景共享 session + 合理 headers 更稳 - 记得调用
session.close()(本例中由上下文管理器自动处理)
遇到 “OSError: [Errno 24] Too many open files” 怎么办
这是典型的系统级资源超限,不是 Python 报错,而是 Linux/macOS 对单进程打开文件数(包括 socket)做了默认限制(通常 1024)。ThreadPoolExecutor 开太多线程 + 每个线程发多个请求,很容易撞上这个墙。
- 先查当前限制:
ulimit -n;临时提高:ulimit -n 4096(仅当前 shell 有效) - 更治本的是降低
max_workers,同时在Session上显式限制连接池大小:session.adapters['https://'].pool_connections = 10、pool_maxsize = 10 - 如果用
urllib3底层,注意PoolManager的maxsize和num_pools也要匹配线程数,否则连接复用率低,照样打满 fd
为什么有时并发数设对了,但实际请求数还是远超预期
因为 ThreadPoolExecutor.submit() 或 .map() 是**立即提交所有任务到队列**,哪怕线程池只有 3 个 worker,你 submit 1000 个 URL,这 1000 个任务对象就立刻生成并持有各自的参数(包括 URL 字符串、闭包变量等),内存占用陡增,且一旦某个任务抛出未捕获异常,可能中断整个批次。
- 用
executor.map()时,输入迭代器会被提前展开(除非是生成器且 executor 内部做惰性处理,但标准库不保证),慎用于超大 URL 列表 - 更可控的方式是分批提交:每次取
min(100, len(urls))个 URL,list(executor.map(...))完再取下一批 - 务必包住任务函数的
try/except,否则一个失败会导致后续结果顺序错乱或丢失,比如用executor.submit()后调用future.result()时再捕获
线程池本身不解决 DNS 解析慢、SSL 握手卡顿、目标服务随机 503 这些问题,max_workers 只是第一道水闸,后面还得靠重试策略、请求间隔、User-Agent 轮换来兜底。

















