直接用 threading.Thread 启动大量线程易崩,因无并发控制导致服务端拒连(429)或本地文件描述符耗尽;应改用 ThreadPoolExecutor 并设 max_workers=5–10,配合 Session 复用连接、合理超时重试及线程安全状态共享。

为什么直接用 threading.Thread 启动一堆线程容易崩
因为没控并发数,API 服务端会拒绝连接或返回 429 Too Many Requests,本地还可能耗尽文件描述符(OSError: [Errno 24] Too many open files)。Python 的 threading 本身不带限流,得自己管线程生命周期和数量。
实操建议:
- 别手写
for i in range(100): threading.Thread(...).start()—— 这等于发洪水 - 改用
concurrent.futures.ThreadPoolExecutor,它内置队列和最大线程数控制 - 设
max_workers为 5–10(多数 HTTP API 的合理并发上限,具体看服务文档) - 配合
requests.Session()复用连接,避免每次新建 TCP 握手开销
ThreadPoolExecutor.map() 和 submit() 怎么选
map() 适合所有请求参数结构一致、顺序重要、且不需要单独处理每个异常的场景;submit() 更灵活,能逐个拿结果、捕获单个请求异常、混用不同参数。
常见错误:用 map() 传一个含多个字段的字典列表,但函数签名不匹配,报 TypeError: my_func() takes 2 positional arguments but 1 was given。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 用
map()时,确保目标函数只接收单个参数(如lambda x: fetch(x["url"], x["headers"])封装好) - 用
submit()时,用as_completed()拿结果,避免按提交顺序阻塞等待 - 别在
submit()回调里再起线程——这会绕过线程池管理,失控
怎么安全地共享状态(比如存响应、计数、重试)
多线程里直接读写全局 dict 或 list 会丢数据,尤其涉及 result.append(...) 或 counter += 1 这种非原子操作。
实操建议:
- 用
threading.Lock()包裹共享写入,例如:lock = threading.Lock() with lock: results.append(data) - 优先用线程安全结构:如
queue.Queue(比 list + lock 更可靠),或concurrent.futures.as_completed()自带的结果迭代器 - 避免在 worker 函数里修改模块级变量——除非加锁,且确认无其他线程访问
- 重试逻辑必须放在 worker 内部(如 requests 的
retry_strategy),别靠外部循环重发任务
超时、重试、SSL 验证这些细节不设好就白忙
默认 requests.get() 无限期卡住,一个慢请求拖垮整个线程池;不关 SSL 验证可能在某些内网环境失败;没设重试,网络抖动就直接报错。
实操建议:
- 每个
requests.get()必须设timeout=(3, 7)(3 秒连通,7 秒读完) - 用
urllib3.util.Retry配合requests.adapters.HTTPAdapter开启重试,禁用raise_on_status=False - 内网或自签证书环境,显式传
verify=False(并加 warning),别依赖默认行为 - 别把 API key 放在闭包变量里传给 worker——用
functools.partial()或封装成类方法更清晰
线程池不是万能加速器,真正瓶颈常在 DNS 解析、TLS 握手或服务端限流。压测前先单请求跑通 timeout 和 retry,再扩并发。


















