多线程不解决速率限制,因限流按IP/Key统计而非线程;需用Semaphore全局控并发、复用Session、遵守Retry-After或指数退避,并解析限流响应头。

为什么直接开多线程反而更容易触发速率限制?
很多开发者以为“开10个线程并发请求,就能把单线程1秒1次的限制变成1秒10次”,结果发现API直接返回 429 Too Many Requests,甚至IP被临时封禁。根本原因是:速率限制通常不是按“进程/线程”计数,而是按 IP、API key 或 user token 统计单位时间内的总请求数。多线程只是让请求发得更猛、更集中,没做任何节流协调,相当于一群人挤在同一个门口抢着进门。
用 threading.Semaphore 控制全局并发请求数
真正起作用的是对“发出请求”这个动作做同步限流,而不是让线程自己睡几秒。用 Semaphore 可以确保任意时刻最多只有 N 个请求真正在发出去(无论有多少线程在等待)。
实操建议:
- 初始化一个全局
semaphore = threading.Semaphore(3),表示最大并发请求数为3(根据API文档的rate limit调整) - 每个工作线程在调用
requests.get()前,先执行semaphore.acquire();成功收到响应或异常后,立刻semaphore.release() - 不要在
acquire()后加time.sleep()—— 那会浪费等待窗口,且不解决排队问题 - 如果API要求“每分钟最多60次”,那就设
Semaphore(1)+ 每次请求后time.sleep(1.0),但更推荐用ratelimit库或令牌桶实现平滑发送
import threading
import requests
<p>semaphore = threading.Semaphore(2) # 全局最多2个并发请求</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/skill9602" title="Session Handoff"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20dbae7c507.jpg" alt="Session Handoff" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/skill9602" title="Session Handoff">Session Handoff</a>
<p>创建全面的交接文档,使新AI代理能够无缝继续工作,消除歧义。解决上下文耗尽问题。
时机:(1)用户请求保存交接,(2)上下文即将满,(3)主要任务完成,(4)会话结束,(5)恢复工作时。
关键词:保存状态、创建交接、上下文已满、暂停、从...恢复、加载交接、保存进度、会话转移、交接</p>
</div>
<a href="/xiazai/skill9602" title="Session Handoff" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>def fetch_url(url):
semaphore.acquire()
try:
resp = requests.get(url, timeout=10)
return resp.json()
finally:
semaphore.release() # 必须放finally里,防异常跳过
别忽略 requests.Session 和连接复用
多线程下频繁创建 requests.Session() 实例会导致端口耗尽、TLS握手开销大,间接加剧被限流风险。复用 Session 能显著降低底层连接压力,也让服务端更容易识别为“同一客户端的合理行为”。
实操建议:
- 创建一个模块级的
session = requests.Session(),所有线程共用 - 给
session设置headers(如User-Agent、Authorization),避免每次重复写 - 可配置
session.mount('https://', requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10))控制连接池大小 - 注意:Session 不是线程安全的——但
get()、post()这些方法是,所以直接共用没问题
遇到 429 后必须做退避重试,不能硬刚
即使做了并发控制,网络抖动或服务端策略突变仍可能导致偶发 429。此时立刻重试只会雪上加霜;正确做法是解析响应头里的 Retry-After,或按指数退避(exponential backoff)等待。
实操建议:
- 检查响应头:
resp.headers.get('Retry-After'),优先遵守服务端建议的等待秒数 - 若无该头,用基础退避:首次等
1s,第二次2s,第三次4s……上限设为60s - 重试逻辑必须封装在单次请求函数内,不要放在多线程外层——否则会阻塞整个线程池
- 记录重试次数,超过3次就放弃并抛出异常,防止无限卡住
最常被忽略的一点:限流不是性能问题,是协议问题。你得像和人打交道一样,看对方的规则说明(X-RateLimit-Limit、X-RateLimit-Remaining 这些响应头),而不是只盯着自己的线程数调多调少。

















