【问题标题】:Java - multithreaded code does not run faster on more coresJava - 多线程代码不会在更多内核上运行得更快
【发布时间】:2023-03-15 06:19:01
【问题描述】:

我只是在 4 核机器上运行一些多线程代码,希望它会比在单核机器上更快。想法是这样的:我有固定数量的线程(在我的情况下,每个核心一个线程)。每个线程执行一个Runnable 的形式:

private static int[] data; // data shared across all threads


public void run() {

    int i = 0;

    while (i++ < 5000) {

        // do some work
        for (int j = 0; j < 10000 / numberOfThreads) {
            // each thread performs calculations and reads from and
            // writes to a different part of the data array
        }

        // wait for the other threads
        barrier.await();
    }
}

在四核机器上,此代码在 4 个线程下的性能比在 1 个线程下的性能更差。即使有CyclicBarrier 的开销,我也会认为代码的执行速度至少应该快2 倍。为什么它运行

编辑:这是我尝试过的繁忙等待实现。不幸的是,它使程序在更多内核上运行更慢(也在单独的问题here 中讨论):

public void run() {

    // do work

    synchronized (this) {

        if (atomicInt.decrementAndGet() == 0) {

            atomicInt.set(numberOfOperations);

            for (int i = 0; i < threads.length; i++)
                threads[i].interrupt();
        }
    }

    while (!Thread.interrupted()) {}
}

【问题讨论】:

  • 你能告诉 use 为什么你希望它在更多内核上运行得更快吗?
  • 这表明每个线程都没有在单独的核心中运行,这是有道理的,因为您没有专门告诉它这样做(并且不能使用标准 Java 1.6)。
  • @Mat:线程越多,每个Runnable 的睡眠时间就越短。由于他们同时睡觉,他们应该更快地“醒来”。
  • @bestsss:既然这么琐碎,为什么不解释一下?
  • 啊,好吧。然后这两者都不是,而是我的幽默类型。

标签: java multithreading


【解决方案1】:

不一定能保证添加更多线程来提高性能。增加线程导致性能下降的可能原因有很多:

  • 粗粒度锁定可能过度序列化执行 - 即,锁定可能导致一次仅运行一个线程。你得到了多线程的所有开销,但没有任何好处。尽量减少持有锁的时间。
  • 这同样适用于过于频繁的屏障和其他同步结构。如果内部 j 循环快速完成,您可能会将大部分时间花在障碍中。尝试在同步点之间做更多的工作。
  • 如果您的代码运行速度过快,可能没有时间将线程迁移到其他 CPU 内核。这通常不是问题,除非您创建了许多非常短暂的线程。使用线程池,或者简单地给每个线程更多的工作会有所帮助。如果您的线程每个运行时间超过一秒左右,这不太可能成为问题。
  • 如果您的线程正在处理大量共享的读/写数据,缓存行弹跳可能会降低性能。也就是说,尽管这通常会导致性能下降,但仅此一点不太可能导致性能比单线程情况更差。尝试确保每个线程写入的数据与其他线程的数据通过缓存行的大小(通常约为 64 字节)分开。特别是,不要像[thread A, B, C, D, A, B, C, D ...] 这样布置输出数组

由于您还没有显示您的代码,所以我无法在此详细说明。

【讨论】:

  • 谢谢!我想我的问题是大部分时间都花在屏障和缓存线弹跳上。我很想展示代码,但即使它只是基本的计算,它也有很多代码......
  • @bdonlan,错误共享与在循环屏障上的紧密等待相结合可能会产生结果,即上下文切换可能比完成的工作更昂贵(缓存垃圾会进一步恶化)。同样在障碍物上等待也会带来额外的切换延迟(对公园来说价值太高)
  • @ryyst,您可以尝试忙等待(认真)而不是循环障碍。
  • @bestsss:您对如何实现忙等待有什么建议吗?
  • @Suhail,这里是维基百科虚假分享的链接:en.wikipedia.org/wiki/False_sharing
【解决方案2】:

你正在睡觉 纳秒,而不是 毫秒

我变了

Thread.sleep(0, 100000 / numberOfThreads); // sleep 0.025 ms for 4 threads

Thread.sleep(100000 / numberOfThreads);

正如预期的那样,与启动的线程数成正比


我发明了一个 CPU 密集型“countPrimes”。完整的测试代码可用here

我在我的四核机器上获得了以下加速:

4 threads: 1625
1 thread: 3747

(CPU 负载监视器确实显示前一种情况下 4 个进程忙,后一种情况下 1 个核心忙。)

结论:您在同步之间的每个线程中完成相对较小部分的工作。同步所花费的时间比实际的 CPU 密集型计算工作要多得多。

(此外,如果您有 内存密集型 代码,例如线程中的大量数组访问,CPU 无论如何都不会成为瓶颈,您也不会看到任何通过将其拆分到多个 CPU 上来加速。)

【讨论】:

  • 嗯,这很糟糕。现在我无法复制了。
  • 我设法使用“countPrimes”方法进行了复制。答案已更新。
  • 你的结论听起来是对的。你有什么建议来解决这个问题(bestsss 提到忙着等待)?
  • Thread.sleep(0, 100000 / numberOfThreads); // 4 个线程睡眠 0.025 毫秒 纳秒睡眠,即 Thread.sleep(long, int) 仅影响毫秒(通过修改 1)代码类似于 if (nanos &gt;= 500000 || (nanos != 0 &amp;&amp; millis == 0)) millis++; 所以它是 1 毫秒
  • @bestsss, 1) 请提供对支持该声明的一些文档的参考。 2)即使是1毫秒,我的观点仍然完全有效:与同步所需的时间相比,时间真的很短。
【解决方案3】:

runnable 中的代码实际上并没有做任何事情。
在您的 4 个线程的具体示例中,每个线程将休眠 2.5 秒并通过屏障等待其他线程。
所以正在发生的一切是每个线程都在处理器上增加i,然后阻塞睡眠,使处理器可用。
我不明白为什么调度程序会将每个线程分配到一个单独的核心,因为所发生的只是线程大部分都在等待。
期望只使用相同的内核并在线程之间切换是公平合理的
更新
刚刚看到您更新的帖子说循环中正在进行一些工作。虽然你没有说,但发生了什么。

【讨论】:

  • 我刚刚意识到,所以我更新了我的问题。我实际上工作,所以操作系统应该为每个线程分配处理能力。
  • @ryyst:但是你在for循环中做了什么?如果你不发布实际代码,我看不出我们如何帮助你进行分析。
  • @user:基本计算,如加/乘ints。没有锁,没有同步,没有 I/O。
  • 仍然只是描述没有帮助。像 int 加法这样的基本计算可以非常快地完成,但实际处理仍然很少,不需要使用另一个核心。
  • @ryyst,显示代码,你可能有错误共享,而且在循环中等待屏障非常昂贵,将屏障移到循环外。
【解决方案4】:

跨核同步比单核同步慢得多

因为在单核机器上,JVM 不会在每次同步期间刷新缓存(非常缓慢的操作)

查看this blog 帖子

【讨论】:

  • JVM 不刷新缓存是什么意思
【解决方案5】:

这是一个未经测试的 SpinBarrier,但它应该可以工作。

检查这是否对案件有任何改善。由于您在循环中运行代码,因此如果您的核心处于空闲状态,那么额外同步只会损害性能。 顺便说一句,我仍然相信您在计算、内存密集型操作中存在错误。你能告诉 你使用什么 CPU+操作系统。

编辑,忘记版本了。

import java.util.concurrent.atomic.AtomicInteger;

public class SpinBarrier {
    final int permits;
    final AtomicInteger count;
    final AtomicInteger version;
    public SpinBarrier(int count){ 
        this.count = new AtomicInteger(count);
        this.permits= count;
        this.version = new AtomicInteger();
    }

    public void await(){        
        for (int c = count.decrementAndGet(), v = this.version.get(); c!=0 && v==version.get(); c=count.get()){
            spinWait();
        }       
        if (count.compareAndSet(0, permits)){;//only one succeeds here, the rest will lose the CAS
            this.version.incrementAndGet();
        }
    }

    protected void spinWait() {
    }
}

【讨论】:

  • 我现在没有时间找出原因,但这不起作用(我尝试了 4 个线程)。不过谢谢!
  • @ryyst,我认为我已经放弃了没有版本的代码,所以当第一个线程赢得 CAS 时,任何其他服务员都会看到恢复的计数回到许可并永远旋转。
  • 仍然对我不起作用...所有 CPU 都是 @ 100% 但没有任何反应。我还更新了我的问题。繁忙的等待机制可以工作,但如果您为其添加更多线程,则运行速度会变慢。
  • @ryyst,只有在核心线程数少于或相等时,忙等待才有效。您确定您创建的 SpinBarrier 具有与您打算启动的相同数量的线程吗?你也不能在忙碌的等待中使用同步,它打败了任何忙碌的想法。
  • 我在四核上,您更新的代码不适用于 2 个线程(它适用于 1 个线程)。另外,在我的代码中,我只使用同步来递减计数器,我忙于在同步部分之外等待,对吧?
猜你喜欢
  • 2018-12-25
  • 1970-01-01
  • 1970-01-01
  • 2017-02-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多