如何合理地估算线程池大小

简介: 如何合理地估算线程池大小

文章目录

CPU密集型

执行结果

图标结果

得出结论

IO密集型

实验(略)

混合型

为什么线程上下文切换的时候会耗费性能

上下文切换的概念

上下文切换带来的损耗

参考文档

CPU密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

写了一个cpu密集型的例子,一直执行自增操作

CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间

如果是CPU密集型应用,则线程池大小设置为N+1;(对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的效率。(即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。摘自《Java Concurrency In Practise》)


以下我们写一个cpu密集型的demo; 来试试不同线程数量的耗时结果

public class SrcTest {
    static int threadNum = 20;
    final static int taskNum = 200;
    static ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
    static CountDownLatch endGate = new CountDownLatch(taskNum);
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i< taskNum; i++){
            executorService.submit(cpuCal());
        }
        endGate.await();
        long end = System.currentTimeMillis();
        System.out.println("线程池数量:"+threadNum+" CPU核数:"+Runtime.getRuntime().availableProcessors()+"全部结束:time:"+(end-start));
    }
    public static Thread cpuCal(){
        return new Thread(()->{
            long start = System.currentTimeMillis();
            //随便写个cpu耗时的操作
            for (int i = 0;i<10000000;i++){
                StringBuffer sb = new StringBuffer();
                sb.append(i).append(",");
            }
            long end = System.currentTimeMillis();
            System.out.println("线程ID:"+Thread.currentThread().getId()+"; 消耗时间:"+(end-start));
            endGate.countDown();
        });
    }
}

执行结果

threadNum=1

image.png

threadNum=2

image.png

threadNum=4

image.png

可以看到在线程数量为4的时候,我的这8核机器中的4个cpu飙升

image.png

threadNum=8

image.png

打开cpu使用率

可以看到在线程数量为8的时候,我的这8核机器中的8个cpu全部满负载运行

image.png

threadNum=14

image.png

threadNum=20

image.png

图标结果

实验系统配置情况:

image.png

物理cpu内核数:4 sysctl hw.physicalcpu

逻辑cpu内核数:8 sysctl hw.logicalcpu


因为开启了 超线程技术 就有了4核8线程


线程数 全部结束耗费时间 单任务平均耗费时间

1 63007 320

2 35828 345

4 24252 430

8 21340 700

14 22837 1100

20 22081 1920

得出结论

通过上面的实验数据,我们分析可以得出

在CPU密集型的场景下; 当线程数=CPU逻辑核数 的时候, 总体耗费的时间是最少的;

并且 当线程数 越来越大的时候, 单任务平均耗时会越来越大,这是因为线程数越多,就会有越多的线程上下文切换,耗费一部分性能;

当 线程数 > CPU逻辑核数时候, 总体耗费的时间已经不会有明显的减少,反而会略微上升,并且 单任务耗时确实逐渐增高的;


所以最终结论: 当CPU密集场景下; 线程数 =CPU逻辑核数时候, 总体效率最高;


当然了我们一般可以设置为 CPU逻辑核数+1 ; 这个1 的原因是:即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费


IO密集型

如果是IO密集型应用,则线程池大小设置为2N+1


如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。


接下来在这个文档:服务器性能IO优化 中发现一个估算公式:


最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目


进一步转换

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目


可以得出一个结论:

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程


实验(略)

混合型

混合型任务 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。


为什么线程上下文切换的时候会耗费性能

上下文切换的概念

先来解释一下什么是上下文切换(context switch)。在多任务处理系统中,作业数通常大于CPU数。为了让用户觉得这些任务在同时进行,CPU给每个任务分配一定时间,把当前任务状态保存下来,当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。之后CPU可以回过头再处理之前被挂起任务。上下文切换就是这样一个过程,它允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。在这个过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行


上下文切换带来的损耗

上下文切换会导致CPU在寄存器和运行队列之间来回奔波。这种消耗可以分为两种


损耗种类 描述

直接损耗 CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉

间接损耗 多核的cache之间得共享数据


相关文章
|
NoSQL Java 应用服务中间件
如何合理地估算线程池大小?
如何合理地估算线程池大小?
114 0
如何合理地估算线程池大小?
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
51 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
23 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
20 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
34 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
38 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
41 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
28 1
|
21天前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
55 0