Java 使用多个物理内核并行化非IO任务的最佳方法:每个线程的工作量相等,还是最大线程数?

Java 使用多个物理内核并行化非IO任务的最佳方法:每个线程的工作量相等,还是最大线程数?,java,multithreading,Java,Multithreading,我们有各种各样的任务,它们可以分成相等的工作单元,不涉及磁盘或网络I/O 想象一下,我们正试图处理70幅图像。我们可以独立处理每个图像。过去我们会这样做: 工作单位数量:70 芯数:16 每根螺纹的工作=ceil(70/16)=5 螺纹数=70/5=14个螺纹 您会注意到,在如此高的内核数机器上,此算法似乎“浪费”了两个内核。因此,有人建议这样做可能更好: 螺纹数=芯数=16 每个螺纹的工作=5(对于6个螺纹)或4(对于10个螺纹) 进一步分析后,这似乎不太有利: 如果任务是CPU限制的

我们有各种各样的任务,它们可以分成相等的工作单元,不涉及磁盘或网络I/O

想象一下,我们正试图处理70幅图像。我们可以独立处理每个图像。过去我们会这样做:

  • 工作单位数量:70
  • 芯数:16
  • 每根螺纹的工作=ceil(70/16)=5
  • 螺纹数=70/5=14个螺纹
您会注意到,在如此高的内核数机器上,此算法似乎“浪费”了两个内核。因此,有人建议这样做可能更好:

  • 螺纹数=芯数=16
  • 每个螺纹的工作=5(对于6个螺纹)或4(对于10个螺纹)
进一步分析后,这似乎不太有利:

  • 如果任务是CPU限制的,那么墙只需要运行所有任务 取决于工作量最大的线程,仍然是5
  • 如果任务是内存受限的,添加更多线程只会增加争用。更糟糕的是,最后只剩下5个线程在运行,这可能无法再使内存总线饱和
  • 算法2没有为操作系统或后台工作留下任何备用核心
  • 请注意,我们不希望为每个图像分配任务对象。这在这里并不重要,但在其他情况下,我们可能有很多图像,不想为每个图像分配一个对象。在这两种算法中,我们只需要分配一个Runnable,它在每个线程中捕获两个整数。我们在固定大小的ThreadPoolExecutor中执行Runnable的实际执行

    不过,我确实想知道像ForkJoinPool或GrandCentralDispatch这样的其他实现在这种情况下可能会做什么

    问题:
    • 是否有理由选择第二种算法而不是第一种算法?它们是什么

    • 是否存在一种共识,即“平均而言”一个比另一个好?为什么?


    我们用Java编程,但我相信这个问题适用于任何具有共享内存线程的语言。

    您考虑过一个新的解决方案吗?你的问题到底是关于什么还不是很清楚。不确定“工作单元”是什么,但听起来像“任务”,所以使用并调用
    submit(task)
    方法(Elliott Frisch提到的
    ForkJoinPool
    是一个
    ExecutorService
    )。我们通常不使用ForkJoinPool,因为任务数量有时会很大,任务创建的开销会成为一个问题。我讨论的两种算法都有内存分配O(线程),而不是O(工作单元)。例如,如果我们想要处理70个图像,我们可以告诉线程1处理列表中的索引[0,4]。ForkJoinPool有时在其他情况下很有用,在这些情况下,需要完成的工作量在单元之间并不相似。也许我遗漏了一些东西,但是如果每个图像都可以独立处理,并且您有70个图像,为什么不创建70个处理图像任务,将它们全部提交到线程池队列中(例如,
    执行器服务
    ),并让线程尽可能地处理任务?您提到了任务数量是一种开销;如果是这样的话,为什么不设置生产者/消费者设置,以便一次只创建这么多任务?这不是一次分配这么多任务,而是分配这么多任务。在本例中,它不是不要紧,但想象一下,如果我们处理数百万个16x16映像。在执行完成之前,将创建数百万个任务对象,并可能升级到旧一代。与上述两种算法不同,这两种算法只为每个线程分配一个可运行的和两个整数。我们使用固定大小的ThreadPoolExecutor来执行工作的实际线程执行。我将编辑问题以使其更清楚。您是否考虑了一个?您的问题到底是关于什么的还不是很清楚。不确定“工作单元”是什么,但听起来像“任务”,所以请使用并调用
    submit(task)
    方法(Elliott Frisch提到的
    ForkJoinPool
    是一个
    ExecutorService
    )。我们通常不使用ForkJoinPool,因为任务的数量有时会很大,任务创建的开销会成为一个问题。我讨论的两种算法都有内存分配O(#线程),而不是O(#工作单元)例如,如果我们想要处理70个图像,我们可以告诉线程1处理索引[0,4]在列表中。ForkJoinPool有时在其他情况下很有用,在这些情况下,需要完成的工作量在各个单元之间并不相似。也许我遗漏了一些东西,但是如果每个图像都可以独立处理,并且您有70个图像,为什么不创建70个处理图像任务,将它们全部提交到线程池队列中(例如,
    执行器服务
    ),并让线程尽可能地处理任务?您提到了任务数量是一种开销;如果是这样的话,为什么不设置生产者/消费者设置,以便一次只创建这么多任务?这不是一次分配这么多任务,而是分配这么多任务。在本例中,它不是不要紧,但想象一下,如果我们处理数百万个16x16映像。在执行完成之前,将创建数百万个任务对象,并可能升级到旧一代。与上述两种算法不同,这两种算法只为每个线程分配一个可运行的和两个整数。我们使用固定大小的ThreadPoolExecutor来执行工作的实际线程执行。我将编辑问题以使其更清楚。