Python中ThreadPool共享requests.Session时的连接回收问题
先结合你的场景和观察来拆解问题:你用一个全局的requests.Session对象,在multiprocessing.dummy.ThreadPool(本质是基于线程的池)里批量发起请求,发现线程数多的时候连接会被回收,线程数少的时候不会,而且Session的ID全程不变,禁用GC也没影响。这背后的核心逻辑和requests底层依赖的urllib3连接池机制直接相关。
先搞清楚Session和连接池的基础逻辑
requests.Session本质是维护了一个urllib3的连接池集合,针对每个不同的(主机、端口、协议)组合,都会有一个独立的连接池。这个连接池的核心参数是pool_maxsize(默认值是10),它决定了针对目标服务器,最多能保持多少个长连接处于复用状态。
连接池的工作流程是这样的:
- 当线程通过Session发起请求时,会先从对应服务器的连接池里尝试获取空闲连接;
- 如果池里有空闲连接,直接复用;
- 如果池里没有空闲,但当前池内的连接数还没到
pool_maxsize,就新建一个连接并加入池; - 如果池内连接数已经达到
pool_maxsize,默认会阻塞等待直到有连接被归还(由其他线程用完放回池);如果等待超时,或者设置了block=False,就会创建临时连接,用完之后直接关闭,不会放回池——这就是你看到的“连接回收”。
对应你的观察点逐一解释
Session对象ID始终相同:
你是在主线程创建的Session,线程池里的所有子线程都是共享这个全局实例的,所以id(s)全程不变是完全正常的,所有请求都复用同一个Session的连接池。高线程数(20)时连接回收,低线程数(5)时不回收:
这刚好对应了默认pool_maxsize=10的限制:- 当线程池大小是5(≤10)时,所有线程的请求都能从连接池获取或创建不超过10个的连接,用完后都能放回池里复用,所以连接不会被关闭;
- 当线程池大小是20(>10)时,同时有20个线程在发起请求,连接池最多只能容纳10个长连接,剩下的10个请求不得不创建临时连接,这些临时连接用完后因为池已经满了,无法放回,只能被主动关闭,也就是你观察到的“回收”。
禁用GC后连接仍会回收:
连接的关闭是urllib3的主动行为,和Python的垃圾回收完全无关——这些临时连接是因为无法进入连接池,被框架直接关闭释放,不是因为GC回收了对象。
实现“连接保持存活直至最大请求数”的方案
如果你想让所有连接都保持存活复用,只需要把连接池的pool_maxsize调整为至少等于你的线程池大小即可。可以通过HTTPAdapter来修改这个参数:
from requests.adapters import HTTPAdapter if __name__ == "__main__": s = requests.Session() # 挂载适配器,设置连接池参数:pool_maxsize和线程池大小一致(20) s.mount('https://', HTTPAdapter(pool_connections=20, pool_maxsize=20)) s.headers.update({'Accept': 'application/json'}) main()
这样调整后,所有线程的请求都能获取到连接池里的连接,用完后放回池复用,不会再出现连接被回收的情况。
另外补充一点:如果你希望连接在空闲一段时间后再关闭,可以调整相关超时参数,但核心需求的解决关键还是匹配pool_maxsize和线程池大小。
内容的提问来源于stack exchange,提问作者GP92

