性能文章>线程数量与执行效率的分析>

线程数量与执行效率的分析原创

https://a.perfma.net/img/2382850
1年前
277312

多线程是否会影响执行效率

前提

今天一位同事问了一个问题感觉比较有趣,问题简化规范后是这样的

在同一机器/操作系统上,使用n个线程处理X个任务是不是和使用n*m个线程处理X个任务效率相同?

这样的观点是基于RR-时间片轮转来进行推导的;我不太认同这种观点,直觉上认为参与处理的任务过多或过少都会影响执行效率;下面先进行这两种观点的推导然后在进行实际的验证

时间片轮转

假设前提:

  1. 操作系统对进程的调度是采用的时间片轮转算法来进行处理的,时间片划分的大小为1s;
  2. 每个Task需要消耗单核CPU执行0.5s
  3. 不考虑线程上下文切换耗时

根据以上的前提可以得出下面这个运行图例:

2Thread执行示例

4Thread执行示例

从上图中可以看到无论是2Thread来进行执行还是4Thread来进行执行对于任务的执行耗时其实都是没有任何影响的,因为这其实是1000个Task与2个执行core之间的关系;

这种说法正确的前提必须是满足假设前提中的3点

线程过多过少都会影响执行效率

这种观点主要是基于《Java并发编程实战》一书中的观点:

推算线程池大小

N~cpu~ :指的是执行机器上的物理核心数,额外注意使用容器启动的核心数
U~cpu~ :指的期望的对CPU的使用率
W/C :指的是等待时间与计算时间的比例,对于计算密集型与IO密集型这个值还有所区别

例子:

N~Thread~ = 2 * 0.8 * (1 + 10/2) = 8

如果需要N~Thread~持续增长时需要W/C比例更大,这是不可能实现的,由于w/c是由于Task决定的;
以上是通过反证法的方式来解释提高N~Thread~ 并不能增加并发执行效率的原因;

实际在运用过程中还需要注意Amdahl定律和线程引入造成的性能开销

Amdahl定律

Amdahl定律就是表达并发执行线程池数并不能提高效率,而是并发度提高才能提高执行效率

线程引入造成的性能开销

频繁的线程切换会引起性能损耗

实例分析



public class ExecutorServiceUtil {

    private static final int taskNum = 100000;
    private static final LinkedBlockingQueue QUEUE = new LinkedBlockingQueue(taskNum + 10000);

    private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
            QUEUE);

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(taskNum);
        for (int i = 8; i <= 8192; i = i * 2) {
            System.out.printf("线程数量为[%s]正在执行... %n", i);
            threadPoolExecutor.setMaximumPoolSize(i);
            threadPoolExecutor.setCorePoolSize(i);
            long st = System.currentTimeMillis();
            extracted(latch);
            latch.await();
            long et = System.currentTimeMillis();
            System.out.printf("线程数量为[%s]执行耗时[%s]ms %n", i, et - st);
            QUEUE.clear();
        }
        threadPoolExecutor.shutdown();
    }

    private static void extracted(CountDownLatch latch) {
        IntStream.range(0, taskNum).forEach(i -> {
            threadPoolExecutor.submit(new Task(latch, i));
        });
    }

    static class Task implements Runnable {
        private int taskId;
        private CountDownLatch latch;

        Task(CountDownLatch latch, int taskId) {
            this.latch = latch;
            this.taskId = taskId;
        }


        @Override
        public void run() {
            doExecute();
            latch.countDown();
        }
    }

    public static void doExecute() {
        int min = 0;
        int max = 100000;
        for (int i = min; i <= max; i++) {
            isPrime2(i);
        }
    }


    public static boolean isPrime2(int n) {
        if (n <= 3) {
            return n > 1;
        }
        int sqrt = (int) Math.sqrt(n);
        for (int i = 2; i <= sqrt; i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }
}

ExecutorServiceUtil.java

这是一个循环求解0~10W之间的素数的程序,下面是分别在Windows/Linux上执行的结果

Linux下的执行结果

Windows下的执行结果

操作系统的线程调度策略还是会影响性能的.Linux的线程调度要稍微比Windows的优秀一点点,下面使用vmstatpidstat分析

  • vmstat
 vmstat -w 1

vmstat执行结果

  • pidstat
 pidstat -p /PID -wtu  5

pidstat执行结果

主要关心cswch/snvcswch/sincs指标,表示的是线程上下文切换的一个频率,在线程不断的增加以后可以看到这几个指标在快速的向上增长
不管是从windows还是linux的一个执行结果来看,线程的增加都会导致程序的下降,但是这种下降并不是很明显,也许是得益于现代操作系统对线程切换的不断优化吧

总结

一个线程池中核心线程数与最大线程数的数量是会受到多个方面因素共同影响的,例如操作系统物理核心逻辑线程 以及任务类型(计算密集型/IO密集型)都有关系,因此在使用线程池时对于线程池的6个参数一定要有思考以后在进行创建相应的线程池;

参考资料

Threads configuration based on no. of CPU-cores
Amdahl’s law
Amdahl’s Law in the Multicore Era
Linux vmstat命令实战详解
Java常见的性能问题和排查

点赞收藏
agmtpoy
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

实现定时任务的六种策略

实现定时任务的六种策略

浅析AbstractQueuedSynchronizer

浅析AbstractQueuedSynchronizer

2
1