为数据的并行处理选择最佳线程数

时间:2014-06-10 19:59:10

标签: java multithreading performance parallel-processing future

我们说我有一个处理100万句话的任务。

对于每个句子,我需要对它做一些事情,无论处理它们的具体顺序如何,它都会产生。

在我的Java程序中,我有一组从我的主要工作块中划分出来的一组未来,用一个可调用来定义要在一大块句子上完成的工作单元,我正在寻找一种优化方法我分配用于处理大块句子的线程数,然后重新组合每个线程的所有结果。

在我看到收益递减之前,我可以使用的最大线程数是多少?

另外,是什么原因导致逻辑分配的线程越多,即一次完成的线程越多,就越不正确?

3 个答案:

答案 0 :(得分:10)

在实践中,可能很难找到最佳线程数,甚至每次运行程序时该数字都可能会有所不同。因此,理论上,最佳线程数将是您机器上的核心的数量。如果您的核心是"超线程" (正如英特尔所说)它可以在每个核心上运行2个线程。然后,在这种情况下,最佳线程数是计算机上核心数的两倍。

Also, what causes the logic that the more threads allocated, i.e. 
more being able to be done at once, to be incorrect?

分配更多线程导致同时完成更多工作的原因是错误的,因为只有1个(或者2个线程,如果核心是"超线程")可以在每个核心上一次运行。

假设我有一个不是超线程的四核机器。在这种情况下,我可以同时运行最多4个线程。所以,我的最大吞吐量应该用4个线程来实现。假如我尝试在同一设置上运行8个线程。在这种情况下,内核将来回调度这些线程(通过上下文切换),并且阻塞一个线程以便让另一个线程运行。因此,最多可以一次运行4个线程的工作。

有关这方面的更多信息,查找"上下文切换"是非常有帮助的。使用Linux内核。这将为您提供有关此主题的所有信息。

另外,请注意,称为"用户级线程"的线程之间存在差异。和#34;内核级线程"。如果您进一步研究这个主题,这是一个重要的区别,但它超出了这个问题的范围。

答案 1 :(得分:4)

您的加载I / O是否受限? I / O限制意味着CPU等待大部分时间进行I / O操作。添加更多线程意味着向I / O子系统或远程服务器等发送更多请求。这可能会产生积极影响,因为对存储的请求可以重新排序和组合(分散收集),但只有在达到最大可能时才会/ O带宽。添加更多线程也可能产生不利影响,例如当在传统硬盘上执行更多随机I / O请求时。

如果您的负载受I / O限制,您可以采用各种方法来优化I / O操作。我的第一选择是以更大的块和更流的方式加载数据(如果可能的话)。如果你有大量的点访问或更多的磁盘,如果只是缺少带宽,接下来就是使用外部索引结构或数据库。无论如何,优化I / O是另一个广泛的话题......

您的负载CPU是否受约束?这意味着处理CPU功率是限制因素,而不是I / O带宽。优化您的I / O子系统在这种情况下毫无意义,您需要更多或更快的CPU,并且需要分配负载。

在您的特定情况下,您可以将所有数据加载到内存中,然后您的加载仅受CPU限制。对于CPU绑定负载,最好使用与计算机中CPU核心数相同的线程计数。选择CPU数量作为线程数是相当直接和明显的。它也在问题Optimal number of threads per core中进行了讨论。

实际上,要在Callable对象中执行任务,请使用以这种方式构造的ExecutorService:

  int maxThreadCount = Runtime.getRuntime().availableProcessors();
  ExecutorService executor = 
    new ThreadPoolExecutor(
      0, maxThreadCount - 1,
      1, TimeUnit.SECONDS,
      new LinkedBlockingDeque<>(maxThreadCount * 2),
      Executors.defaultThreadFactory(),
      new ThreadPoolExecutor.CallerRunsPolicy());

现在通过添加任务来完成处理,并等到一切都完成:

  while (moreToDo) {
    Callable c =...
    executor.submit(c);
  }
  executor.shutdown();
  executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);

线程池参数有点棘手。这是一个详细的解释:

通过使用new ThreadPoolExecutor.CallerRunsPolicy(),当池中的所有线程都在使用时,任务生成器线程将停止生成新任务。更确切地说,当达到队列限制时,调用线程也将执行任务。

maxThreadCount - 1:由于我们还使用调用者线程,因此线程池的大小减少了一个。

new LinkedBlockingDeque<>(maxThreadCount * 2):对于阻塞队列的队列大小,选择一个小值,这个想法是,通过在队列中放置一些任务,池线程在调用者线程处理作业时获得新作业本身。如果任务在运行时间上非常不规则,那么这并不完全是完美的。对于此用例,ThreadPoolExecutor应采用更清晰的方法。更好的方法是使用SnychronosQueue并使提交等待,直到线程可用。然而, ThreadPoolExecutor没有&#34;总是排队&#34;相反,如果队列不可能,它会尝试排队并调用RejectionPolicy。

这应该在你的场景中完成。

如果您事先不知道它是CPU绑定还是I / O限制,可能会有负载,并且,为了使事情复杂化,负载可能会在处理过程中改变其行为。我想解决这个问题的方法是使用类似于TCP congestion avoidance algorithm中的方法的自适应算法。 TCP中的拥塞避免完全是同一类问题:&#34;我想要最大吞吐量,但我不知道我的资源&#34;。有人在研究这个吗?

答案 2 :(得分:0)

  

另外,是什么原因导致逻辑分配的线程越多,即一次完成的线程越多,就越不正确?

您是否在问,当T是仅使用一个线程进行相同计算的时间时,N核心机器上运行N个线程的计算为什么需要比完成T / N时间更长的时间?

谷歌“阿姆达尔定律”。很少有100%的工作可以并行完成。通常有一些东西,即使它只是启动/关闭逻辑,必须连续完成。当你测量加速比时,必须连续完成的比特会产生很大的影响。