3. 核心参数详解(上):max_workers 参数对性能的影响

各位同学,今天我们来聊聊 ThreadPoolExecutor 里最核心的参数——max_workers。

说实话,这个参数我见过太多人随便填了。有人直接写 10,有人写 100,还有人直接写 1000。结果呢?要么跑得比单线程还慢,要么直接把服务器搞挂了。

我自己刚入行那会儿也犯过这错。记得有一次线上服务,我图省事把 max_workers 设成了 200,结果 CPU 直接飙到 99%,响应时间从 50ms 变成了 5s。嗯,那天晚上我加班到凌晨两点。

3.1 max_workers 到底控制什么?

简单说,max_workers 决定了线程池里最多能同时跑多少个线程。

你想想看,线程池就像一个餐厅。线程就是服务员,任务就是客人。max_workers 就是餐厅最多能同时服务的客人数量。

服务员太少,客人排队等太久。服务员太多,后厨挤不下,反而互相干扰。

核心结论:max_workers 不是越大越好,也不是越小越好。它需要根据你的任务类型和硬件资源来精确设置。

3.2 如何根据 CPU 核心数设置?

这里有个经典公式,我直接给你:

# CPU 密集型任务
max_workers = cpu_count()  # 或者 cpu_count() + 1

# I/O 密集型任务
max_workers = cpu_count() * (2 ~ 4)  # 甚至更高

为什么会这样?

CPU 密集型任务,说白了就是一直在算。比如视频编码、矩阵运算。这时候线程数超过 CPU 核心数,反而会因为上下文切换变慢。

我做过一个实验:8 核机器跑纯计算任务。

max_workers 完成时间(秒) CPU 利用率
1 80 12%
8 10 98%
16 11 99%
32 13 99%

看到了吗?从 8 到 16,时间反而变长了。这就是上下文切换的代价。

我的习惯:CPU 密集型任务,我一般用 os.cpu_count() 或者 os.cpu_count() + 1。加 1 是为了应对偶尔的 I/O 等待。

3.3 I/O 密集与 CPU 密集场景的区别

这两个场景,说白了就是「等」和「算」的区别。

I/O 密集型:任务大部分时间在等。等网络响应、等磁盘读写、等数据库查询。这时候 CPU 其实是空闲的。

CPU 密集型:任务大部分时间在算。CPU 一直在满负荷运转。

我举个例子你就明白了:

# I/O 密集型:下载文件
def download_file(url):
    response = requests.get(url)  # 90% 时间在等网络
    return response.content

# CPU 密集型:计算哈希
def compute_hash(data):
    result = hashlib.sha256(data)  # 100% 时间在算
    return result.hexdigest()

对于 I/O 密集型,你可以开很多线程。因为线程在等 I/O 的时候,CPU 可以切到别的线程干活。

我曾经优化过一个爬虫项目。一开始 max_workers=10,每秒只能爬 50 个页面。后来我改成 max_workers=100,每秒能爬 800 个页面。为什么?因为每个请求 90% 的时间都在等网络,100 个线程其实只有 10 个在真正干活。

避坑指南:我曾经犯过一个错——在 I/O 密集型任务里把 max_workers 设得太大。比如 8 核机器设成 1000。结果呢?线程创建开销太大,内存暴涨,反而比 100 个线程还慢。

所以,I/O 密集型也不是越大越好。一般建议 cpu_count() * 5 起步,然后压测调优。

3.4 一个实用的判断方法

怎么判断你的任务是 I/O 密集还是 CPU 密集?

我教你一个土办法:

  1. 先跑一次任务,记录总耗时 T1
  2. 把任务改成只做计算部分(去掉 I/O),记录耗时 T2
  3. 如果 T2 / T1 < 0.3,说明是 I/O 密集型
  4. 如果 T2 / T1 > 0.7,说明是 CPU 密集型

这个方法虽然粗糙,但够用。我在项目里一直这么干。

3.5 本章知识体系

下面这张图,帮你理清今天讲的核心逻辑:

max_workers 参数调优决策树 max_workers 设置 CPU 密集型 I/O 密集型 公式:cpu_count() 或 +1 特点:线程数 ≈ 核心数 公式:cpu_count() * 2~5 特点:线程数可远超核心数 核心原则:先判断类型,再设置参数

这张图的核心逻辑很简单:先判断你的任务是 CPU 密集还是 I/O 密集,然后套用对应的公式。别一上来就拍脑袋填数字。

最后说一句:公式只是起点。真正的最佳值,要靠压测来验证。下一节我们会讲怎么用压力测试找到那个「黄金数字」。

好了,今天的内容就到这里。记住一句话:max_workers 不是越大越好,合适才是最好。

课后思考:如果你的任务既有 CPU 计算,又有 I/O 等待(比如先下载再解析),该怎么设置 max_workers?