IO密集型任务该如何设置线程池线程数
任务类型
CPU密集
CPU密集型的话,一般配置CPU处理器个数+/-1个线程,所谓CPU密集型就是指系统大部分时间是在做程序正常的计算任务,例如数字运算、赋值、分配内存、内存拷贝、循环、查找、排序等,这些处理都需要CPU来完成。
IO密集
IO密集型的话,是指系统大部分时间在跟I/O交互,而这个时间线程不会占用CPU来处理,即在这个时间范围内,可以由其他线程来使用CPU,因而可以多配置一些线程。(线程处于io等待或则阻塞状态时,不会占用CPU资源)
混合型
混合型的话,是指两者都占有一定的时间。
实际上工作中的大部分场景中,线程池的能力往往会超出想象。
测试准备
下面的计算方式很粗略,而且有漏洞,但是也可以作为一个参考
处理器信息
四核8线程 (超线程)
任务示例
我们首先确认一下单个任务的io时间占比,下面是测试代码
class ThreadPoolTest {
public static int PARK_TIME = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
runTask(1);
}
public static void runTask(int threadNum) throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
threadNum, threadNum, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100)
);
long start = System.currentTimeMillis();
List<Future<?>> taskList = new ArrayList<>();
for (int i = 0; i < 1; i++) {
taskList.add(threadPoolExecutor.submit(() -> {
doJob();
}));
}
for (Future<?> future : taskList) {
future.get();
}
long time = System.currentTimeMillis() - start;
System.out.println(threadNum + "个线程,耗时:" + time + "停顿占比" + (PARK_TIME * 100.0 / time));
threadPoolExecutor.shutdown();
}
public static Long doJob() {
long result = 0L;
PARK_TIME = 0;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
if (i % 10_000_000 == 0) {
try {
++PARK_TIME;
// 模拟IO
LockSupport.parkNanos(100_000_000);
} catch (Exception ignore) {
}
}
result += i;
}
return result;
}
}
执行结果
直接运行 输出如下:
1个线程,耗时:27862停顿占比0.7716603258918958
也就是说大概77%的时间线程在睡觉。
分析
按我电脑的配置可以认为核心数coreNum为8, 假设任务数够多的情况下。不考虑上下文切换等的耗时,单个任务io耗时占比为x,在线程数最少的情况下想让cpu利用率达到最高,可以得出一个等式 1 / (1 - x) * coreNum = 100% * coreNum(我们假设线程在活跃状态时能完全占用单个核心)。代入上面得到的值 x = 0.77, coreNum = 8 可以比较容易的算出来如果想让cpu利用率达到最高, 1 / (1 - 0.77) * 8 约等于34。即线程池的线程数设置为35比较合理。在我的电脑上合适的线程数和任务io耗时占比x的关系大致可以认为 1 / (1 - x) * 8,即理论上的图如下,这是一个非常粗糙的等式,实际上随着线程数增多,上下文切换带来的开销越来越大,和下面这张图的出入还是蛮大的。
不同线程数下的程序总执行耗时
下面简单修改下程序来验证一下任务量固定,不同线程数下的程序执行耗时。
代码
class ThreadPoolTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
List<Integer> threadNumList = Arrays.asList(4, 8, 16, 25, 34, 50);
for (Integer threadNum : threadNumList) {
runTask(threadNum);
}
}
public static void runTask(int threadNum) throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
threadNum, threadNum, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100)
);
long start = System.currentTimeMillis();
List<Future<?>> taskList = new ArrayList<>();
for (int i = 0; i < 55; i++) {
taskList.add(threadPoolExecutor.submit(() -> {
doJob();
}));
}
for (Future<?> future : taskList) {
future.get();
}
long time = System.currentTimeMillis() - start;
System.out.println(threadNum + "个线程,耗时:" + time);
threadPoolExecutor.shutdown();
}
public static Long doJob() {
long result = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
if (i % 10_000_000 == 0) {
try {
LockSupport.parkNanos(100_000_000);
} catch (Exception ignore) {
}
}
result += i;
}
return result;
}
}
执行结果
4个线程,耗时:367002
8个线程,耗时:190751
16个线程,耗时:108358
25个线程,耗时:78632
34个线程,耗时:53151
40个线程,耗时:53563
45个线程,耗时:55196
50个线程,耗时:55729
期间cpu占用情况如下
总结
1.线程数从4-34期间耗时基本上稳步缩减,但是线程数从34变成50的时候耗时并没有明显减少,反而有增加的趋势,只有cpu利用率一直在飙升。
io密集型任务线程池任务的确有一个较优解的,超过这个边界再继续增加线程数,算力会被上下文切换给浪费掉,在执行CPU密集型任务时这个现象会更加明显。
2.即使是50个线程的时候,算力依然有剩余,并没有达到100%利用率。
这是因为,单个线程在活跃状态下也并不能完全占用单个核心的所有时间片
3.每次任务执行完都有一个小落差,这个可以自己思考一下为什么。
附:
不同线程执行耗时 以及资源利用率
34个线程,耗时:60389
35个线程,耗时:54077
36个线程,耗时:54886
37个线程,耗时:55035
38个线程,耗时:55231
39个线程,耗时:53961
40个线程,耗时:53701
41个线程,耗时:54406
42个线程,耗时:54794
43个线程,耗时:53585
44个线程,耗时:52690
45个线程,耗时:55242