java—在使用threadpoolexecutor时,无法看到cpu绑定任务的上下文切换开销

nhjlsmyf  于 2021-06-30  发布在  Java
关注(0)|答案(4)|浏览(294)

我试着做一个简单的实验,当你有一堆cpu密集型任务时,我想找出线程池的合适大小。
我已经知道这个大小应该等于机器上的内核数,但我想用经验证明这一点。代码如下:

public class Main {

    public static void main(String[] args) throws ExecutionException {
        List<Future> futures = new ArrayList<>();
        ExecutorService threadPool = Executors.newFixedThreadPool(4);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 100; i++) {
            futures.add(threadPool.submit(new CpuBoundTask()));
        }

        for (int i = 0; i < futures.size(); i++) {
            futures.get(i).get();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Time = " + (endTime - startTime));
        threadPool.shutdown();
    }

    static class CpuBoundTask implements Runnable {
        @Override
        public void run() {
            int a = 0;
            for (int i = 0; i < 90000000; i++) {
                a = (int) (a + Math.tan(a));
            }
        }
    }
}

每个任务在大约700毫秒内执行(我认为这足以被threadscheduler抢占至少一次)。
我在macbookpro 2017上运行这个,3.1 ghz intel core i5,2个物理内核,激活了超线程,所以有4个逻辑CPU。
我调整了线程池的大小,并多次运行这个程序(平均计时)。结果如下:

1 thread = 57 seconds
2 threads = 29 seconds
4 threads = 18 seconds
8 threads = 18.1 seconds
16 threads = 18.2 seconds
32 threads = 17.8 seconds
64 threads = 18.2 seconds

当我添加这么多线程(超过cpu内核的数量)时,我本来以为执行时间会大大增加,这是因为上下文切换的开销,但似乎这并没有真正发生。
我使用visualvm来监视程序,看起来所有的线程都被创建了,它们都处于运行状态,正如预期的那样。此外,cpu似乎使用得当(接近95%)。
有什么我不知道的吗?

yfjy0ee7

yfjy0ee71#

executors.newworkstealingpool项目

如果您使用的是Java8,请使用 workStealingThreadPool 因为它可能会产生最好的结果:

ExecutorService es = Executors.newWorkStealingPool();

使用所有可用的处理器作为其目标并行级别来创建工作线程池。并行级别对应于主动参与或可用于参与任务处理的最大线程数。线程的实际数量可能会动态地增长和收缩。偷工池不能保证提交任务的执行顺序。

nuypyhwy

nuypyhwy2#

首先,上下文切换开销随着线程数的增加而增加的假设并不总是正确的。您的示例程序执行固定数量的工作。线程越多—每个线程所做的工作越少,接收的cpu时间也越少。
即使有数百个线程,操作系统也不会无限频繁地在它们之间切换。通常允许线程在没有抢占的情况下运行的最小间隔(时间片)。由于有太多线程争夺一个物理内核,每个线程接收cpu时间片的频率会降低(即饥饿),但上下文切换的数量不会与线程的数量成比例增长。
我测量了linux程序中上下文切换的数量 perf :

perf stat -e context-switches java Main

结果如下:

2 threads | 1,445 context-switches
 4 threads | 2,417 context-switches
 8 threads | 9,280 context-siwtches
16 threads | 9,257 context-switches
32 threads | 9,527 context-switches
64 threads | 9,986 context-switches

当线程的数量超过物理cpu的数量时,上下文切换的一个巨大飞跃就会发生,但是之后这个数量并没有增长那么多。
好的,我们可以看到大约10k的上下文切换。有那么多吗?答案表明,上下文切换的延迟可以估计为几微秒。我们以10为上限。因此,10k交换机加起来大约需要100ms,即每个cpu需要25ms。您的测试不太可能检测到这种开销。此外,所有线程都是纯cpu绑定的—它们甚至不能访问足够的内存,从而受到cpu缓存污染的影响。它们也不访问其他共享资源,因此在本例中没有间接的上下文切换开销。

vyswwuz2

vyswwuz23#

当我添加这么多线程(超过cpu内核的数量)时,我本来以为执行时间会大大增加,这是因为上下文切换的开销,但似乎这并没有真正发生。
由于很多原因,很难发现这一点。首先,现代操作系统非常擅长针对这个用例进行优化。上下文切换曾经是一个大难题,但在现代内存体系结构中,这样做的成本要低得多。
上下文切换的代价是内存缓存刷新。当一个线程被交换到一个cpu中时,本地缓存内存可能不包含它进行计算所需的任何每线程信息。它必须去主存读取所需的内存行,这是较慢的。交换出来的速度也比较慢,因为任何脏页都必须写入主内存。出于这个原因,我认为如果任务使用更多的缓存内存,您可能会看到更高的上下文切换代价。当前程序只存储几个整数。例如,假设您在程序开始时为每个线程分配~10k,并将随机值放入其中。然后,当每个线程运行时,它们会尝试随机访问相应的10k块中的数据,这些块将移动到cpu缓存内存中。那也许是个更好的实验。但也就是说,您必须对您的体系结构有很多了解,并适当地优化应用程序,以完全检测上下文切换。
最后,像任何java测试程序一样,您应该运行一分钟,以便类热交换和其他优化得到解决,然后运行一段长时间收集数据。运行一个耗时18秒的测试比测试代码更能锻炼jvm。如果你跑1800秒,你可能会看到一些可以测量的差别。正如@dreamcrash提到的,使用 System.nanoTime() 应该用于这样的细粒度计时计算。

14ifxucb

14ifxucb4#

在这种情况下,应该使用system.nanotime()而不是system.currenttimemillis()。
你的算法停止在 4 为了简单起见,让我们假设所有线程执行相同数量的任务,因此每个线程25个。每一根线 18 计算25次迭代所需的时间。
以一种非常简单的方式,当你和 64 线程,每个核心有8个线程,第一个 4 迭代次数有 4 并行运行的线程(每个核1个)和其他线程 60 线程处于空闲模式,等待cpu资源计算其迭代次数,因此您有如下情况:

Iteration 0 : Thread 1 (running)
Iteration 1 : Thread 2 (running)
Iteration 2 : Thread 3 (running)
Iteration 3 : Thread 4 (running)
Iteration 4 : Thread 5 (waiting)
Iteration 5 : Thread 6 (waiting)
Iteration 6 : Thread 7 (waiting)
Iteration 7 : Thread 8 (waiting)
...
Iteration 63 : Thread 64 (waiting)

当那些 4 线程完成了它们的迭代,它们将得到另一个迭代。同时,让我们说 58 当其他线程被阻塞等待cpu时,开始处理接下来的四个迭代(同样是4个线程并行执行工作),依此类推。所以你总是这样 4 线程并行运行,这就是为什么:

8 threads = 18.1 seconds
16 threads = 18.2 seconds
32 threads = 17.8 seconds
64 threads = 18.2 seconds

你有大约相同的执行时间,大约相同的执行时间 4 螺纹加工 25 并行迭代。
因为这是一个cpu受限的算法,没有以下问题:
同步化;
加载不平衡(即,每个循环迭代需要大约相同的执行时间);
内存带宽饱和;
缓存失效;
虚假分享。
当您增加每个线程的线程数时,它不会在总体执行时间上反映太多 core .

相关问题