Java多线程在100% CPU使用率下的优化

5
我有一个应用程序,它接受队列中的工作,并将该工作分配给独立线程来完成。线程数量不是很多,最多达到100个,但这些任务很密集,并且可以很快将CPU推高到100%。
为了尽快完成大量工作:我是否最好在需要更多工作时启动更多线程,并让Java线程调度程序处理工作分配,或者更智能地管理工作负载以使CPU保持在100%以下并让我更快地完成工作?
该机器专用于我的Java应用。
编辑:感谢您的精彩建议!任务的复杂性和涉及I/O,因此具有较低的线程池(例如4)可能仅将CPU运行到20%。我无法知道有多少任务会实际将CPU推到100%。我想知道是否应该通过RMI监视CPU并动态调整工作量,还是只需让操作系统处理即可。

4
请使用一个固定大小的线程池,其中线程数与处理器数量相同。100% 的 CPU 使用率不一定是坏事,但你不能仅通过查看 CPU 使用率来判断是否达到最佳 CPU 利用率,因为过载情况(如抖动)也可能导致 CPU 使用率达到 100%。 - trutheality
我以为你正在使用一个拥有100个线程的池。是每个任务都创建了一个新的线程吗?如果是这样,请立即停止并按照@trutheality的建议使用池。 - Martin James
关于你的编辑,@Steve,如果你能将你的线程分为CPU密集型和IO线程,那么你可以将CPU密集型线程放入一个#-of-cores池中,让IO线程无需池子自行生成。这理论上应该会给你最佳的使用效果。 - trutheality
5个回答

15
如果在并行线程中有太多同时进行的计算密集型任务,你很快就会达到收益递减点。实际上,如果有N个处理器(核心),那么你不希望有超过N个这样的线程。现在,如果任务偶尔暂停以进行I/O或用户交互,则正确的数量可能会稍微大一些。但是通常情况下,如果在任何时刻有更多的线程想要进行计算,而可用核心数不足,则你的程序浪费时间进行上下文切换--即,调度成本很高。

2
多少钱?如果有很多CPU密集型任务,盒子会超载,并且总是有比处理器更多的准备线程,那么操作系统很可能会在每个imer中断时更改准备线程集。因此,无论有多少CPU绑定线程,上下文切换的数量都将限制为每30ms或其他时间一次。这不重要。您上面所说的是正确的,但并不是用户进行额外线程微观管理的任何理由(这往往会出错)。 - Martin James
1
@MartinJames -- 我认为选择一个最优大小的线程池并不是微观管理。无论如何,请提供引用。你的说法与每个标准文本和参考资料都相悖。只有一个同意我的例子:http://www.ibm.com/developerworks/library/j-jtp0730/index.html。 - Ernest Friedman-Hill
1
更多经验结果。C++ CPU密集型任务。i7,3GHz,4核心(8个超线程),12GB RAM。ticks/poolThreadCount/taskManagerCPU:356/8/34,287/16/29,280/80/30,284/800/28。最佳的池线程计数大于[核心数],而且显著。到目前为止,如果想让CPU密集型任务尽可能快地运行,请使用80个线程。如果想让它们尽可能高效地运行,请使用800个线程。即使是我觉得这不合理,所以有人证明我错了吧... - Martin James
1
@Gray - 事实证明如此。我的任务很无聊,只是做一些常规的工作——增加一个整数成员。我在for循环中添加了另一个“0”。现在它可以计数到100000000,并且我排队了400个。测试期间我的CPU使用率已经达到了所有4/HT8核心的100%。到目前为止,每个线程的Ticks/threadCount分别为:21922/8、20424/80、20191/800。使用800个线程更好!CPU风扇发出很大的噪音,这里变得越来越热了。 - Martin James
2
这是一个有趣的问题。多年来,我看到过关于为了获得最佳性能而仅创建[核心数]个线程的帖子,因为存在“上下文切换开销”的问题。现在我似乎发现,即使对于CPU密集型任务,使用比这更多的线程才是最好的选择。 - Martin James
显示剩余7条评论

8

你的CPU运行在100%并不能说明它们正在进行多少有用的工作。在你的情况下,你使用了比核心数更多的线程,因此100%包括一些上下文切换并且不必要地使用内存(对于100个线程来说影响较小),这是次优的。

对于CPU密集型任务,我通常使用这个习语:

private final int NUM_THREADS = Runtime.getRuntime().availableProcessors() + 1;
private final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

正如其他人所指出的,使用更多线程只会引入不必要的上下文切换。

显然,如果任务涉及到一些I/O和其他阻塞操作,则这不适用,并且更大的线程池将是有意义的。

编辑

为了回复@MartinJames的评论,我运行了一个(简单的)基准测试 - 结果显示从池大小 = 处理器数量 + 1到100仅略微降低了性能(我们称之为5%) - 转向更高数字(1000和10000)确实会显着影响性能。

结果是10次运行的平均值:
线程池大小:9:238毫秒。//(NUM_CORES + 1)
线程池大小:100:245毫秒。
线程池大小:1000:319毫秒。
线程池大小:10000:2482毫秒。

代码:

public class Test {

    private final static int NUM_CORES = Runtime.getRuntime().availableProcessors();
    private static long count;
    private static Runnable r = new Runnable() {

        @Override
        public void run() {
            int count = 0;
            for (int i = 0; i < 100_000; i++) {
                count += i;
            }
            Test.count += count;
        }
    };

    public static void main(String[] args) throws Exception {
        //warmup
        runWith(10);

        //test
        runWith(NUM_CORES + 1);
        runWith(100);
        runWith(1000);
        runWith(10000);
    }

    private static void runWith(int poolSize) throws InterruptedException {
        long average = 0;
        for (int run = 0; run < 10; run++) { //run 10 times and take the average
            Test.count = 0;
            ExecutorService executor = Executors.newFixedThreadPool(poolSize);
            long start = System.nanoTime();
            for (int i = 0; i < 50000; i++) {
                executor.submit(r);
            }
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);
            long end = System.nanoTime();
            average += ((end - start) / 1000000);
            System.gc();
        }
        System.out.println("Pool size: " + poolSize + ": " + average / 10 + " ms.  ");
    }
}

也许你可以发布你的基准代码 - 我也可以试试!你如何衡量性能? - Martin James
@MartinJames 你是什么意思?代码在我的答案中。唯一的注意事项是,对于更高数量的线程,即使在调用System.gc()之间,GC也会启动。 - assylias
嗯..关于GC的观点很好 - 忘记了它可能的影响。我会将您的基准测试翻译成C++(没有GC),并尝试一下。 - Martin James
@MartinJames 好的,可以理解;)请注意,在我的测试中,效果似乎略高,可能是执行器引起的开销。但我同意20或100并没有显著差异。 - assylias
也许执行者不知道。我的C++线程池是一个简单的生产者-消费者队列,线程在等待工作,所以是的,Java ExecutorService有更多的功能,因此可能会有更多的开销。无论如何,我忘记为您出色的调查点赞了,现在我已经点赞了 :) - Martin James
显示剩余3条评论

7
为了尽快完成最多的工作:当我需要做更多的工作时,是不是只要启动更多的线程,让Java线程调度程序处理分配工作就可以了?或者,如果更聪明地管理工作负载以保持CPU在100%以下,是否能更快地使我取得进展?
随着添加越来越多的线程,上下文切换、内存缓存刷新、内存缓存溢出以及内核和JVM线程管理所产生的开销会增加。当线程占用CPU时,它们的内核优先级降至某个最小值,并达到时间片最小值。随着越来越多的线程挤占内存,它们会溢出各种内部CPU内存缓存。CPU需要从较慢的内存中交换作业的机会更大。在JVM内部,互斥体本地争用更多,可能会有一些(也许是很小的)每个线程和对象带宽GC开销的增量。根据您的用户任务同步情况,更多的线程会导致增加的内存刷新和锁争用。
对于任何程序和任何架构,都存在一个甜点,其中线程可以最优地利用可用的处理器和IO资源,同时限制内核和JVM开销。反复找到这个甜点需要多次迭代和一些猜测。
我建议使用Executors.newFixedThreadPool(SOME_NUMBER);并将您的工作提交给它。然后,您可以进行多次运行,上下变动线程数,直到找到根据工作和盒子架构同时运行的最优池的数量。
但请注意,最优线程数将根据处理器数量和其他可能不容易确定的因素而异。如果线程在磁盘或网络IO资源上阻塞,可能需要更多的线程。如果它们正在执行的工作主要是CPU密集型,则需要更少的线程。

嗯?上下文切换仅在中断时进入操作系统时发生。如果有一组大量CPU密集型的就绪线程,则操作系统将在运行集合周围进行交换(可能主要是在计时器中断后)。一旦就绪线程集合大于核心数,随着添加更多线程,上下文切换开销几乎保持不变。 - Martin James
对我而言,上下文切换是在 CPU 级别发生的,当线程因为 IO 而让出或计时器触发并且运行队列中有太多作业时。这会清除缓存内存(L1),将运行状态复制到内存中,并交换下一个作业。我同意 JVM/OS 有开销,但更复杂的是要考虑缓存内存溢出、CPU 优先级惩罚和 JVM 开销。但是在我的回答中,我应该更多地谈论限制。 - Gray
在这些CPU密集型作业的情况下,当盒子超载时,红色线程比核心多得多,每次更改运行集时都会产生开销,就像你所描述的那样。这只能发生在硬件中断或系统调用中。CPU密集型任务通常不会频繁进行系统调用,因此只剩下中断。如果我们忽略页面错误,CPU密集型任务也不会做太多IO操作,因此只剩下计时器中断。其频率与准备好的线程数无关,因此它生成的开销与准备好的线程数无关。 - Martin James
我同意,尽管时间片窗口不是固定的。随着内核惩罚CPU绑定作业,它会降至某个底部。此外,我并不100%确定页面错误可以被忽略。我不确定CPU/内核是否有能力在调度中使用内存位置 - 可能不会。 - Gray
哦,虽然CPU绑定的作业不会进行系统调用,但它们可能会处理内存屏障或锁定,这也会导致JVM中断。 - Gray

2
“如果我变得更聪明,管理工作负载以使CPU保持在100%以下,这样能让我更快地进展吗?”
可能不会。
正如其他人所说,如果大多数任务都是CPU密集型的话,100个线程对于线程池来说太多了。在典型系统上,这不会对性能产生太大影响 - 如果超载太多,在4个线程和400个线程下都会很糟糕。
您是如何决定使用100个线程的?为什么不是16个呢?
“线程数量并不是很多,最多不超过100”-是否有所变化?只需在启动时创建16个线程,并停止管理它们-只需将队列传递给它们并忘记它们即可。
可怕的想法-您难道为每个任务创建一个新线程吗?

为什么是16?动态调整线程数以适应可用处理器数量确实有意义 - 例如,为什么不使用(num_processor * 1.5)呢? - assylias
处理器数量 * 1.5 - 很好。处理器数量 * 15,其实差别不大。是的 - 从sysinfo获取处理器数量,将其加倍并加上您最初想到的数字<g>。 - Martin James

0

你应该保持100%的使用率,但线程数量要尽可能少。100个线程看起来太多了。


2
为什么?假设有足够的RAM来容纳所有堆栈等,没有持续的分页,100个线程有什么问题吗? - Martin James
1
@assilias 已经证明在 Java 中100个线程并不是一个重要的问题。而在 C++ 中,2000个线程也不是一个重要的问题。 - Martin James

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接