Java并发:用很少的线程执行许多“无限”任务

时间:2013-08-05 13:16:46

标签: java concurrency executors

我正在为根据牛顿定律在空间中移动的一组N个粒子构建(并发)模拟器。 我的想法是将每个粒子建模为一个任务,它与其他粒子(任务)相互作用,以获得它们的位置和质量,以便计算它所受的净力。 每个粒子任务都是

while(true){
   force = thisParticle.calculateNetForce(allTheParticles);
   thisParticle.waitForAllTheParticlesToCalculateNetForce(); // synchronization
   thisParticle.updatePosition(force);
   thisParticle.waitForAllTheParticlesToUpdateTheirState(); // synchronization
}

我可以拥有大量粒子(100或更多),因此我无法创建如此多的Java线程(映射到物理线程)。 我的想法是使用Runtime.getRuntime().availableProcessors()+1个线程来执行许多任务。

但是,我不能使用FixedThreadExecutor,因为粒子任务不会结束。我想使用FixedThreadExecutor,它必须也能够在内部执行某种调度。你知道为此目的吗?

或者,您是否可以通过并发的角度(例如,不同的任务分解)向我建议更好的方法来建模这样的系统?

P.s。:我仅限于“经典”并发机制,不包括演员或类似架构。

7 个答案:

答案 0 :(得分:5)

性能的最大杀手可能是您执行的线程安全检查,以确保所有粒子以线程安全的方式进行交互。我建议你每个核心使用一个线程,并尽量减少线程之间的交互。这可以通过将空间划分为线程来完成,例如半X,半Y,半Z将空间划分为8.您可以同时独立地查看每个空间中的所有交互,只需要在粒子从一个空格/线程传递到另一个空间/线程时担心。

答案 1 :(得分:3)

我会假设您将所有粒子存储在一个二维数组中?这将是Fork-Join Framework的一个很好的候选人。

您可以将数组部分的计算分成更小的部分。你继续分裂直到一定的大小。最后你计算并返回。然后,返回的值将与树的另一侧连接并计算。

答案 2 :(得分:2)

我会创建一个具有适当线程数的ExecutorService,而不是每个粒子的一个线程。我会将粒子保存在列表(或其他类型的集合)中。我将为每个粒子的计算和更新步骤创建要执行的单独工作(作为Runnable或Callable)。当你向执行者提交一份工作时,你会得到一个未来。把这些未来放在一个集合中。在您提交了要并行运行的所有工作后,您将遍历您的期货列表并在每个期货上调用get()以实现同步步骤。

你最终可能会创建一个小的POJO来关联一个粒子和它的计算力(或者隐藏粒子实例中的计算力)。

答案 3 :(得分:1)

为什么不以离散步骤进行计算?

while(true){


for(Particle p : allParticles){
   force = p.calculateNetForce(allParticles);   
   p.setNextPosition(force); //Remembers, but doesn't change the current position
}

for(Particle p : allParticles){
    p.nextState(); //Change the position
}

}

首先计算每个粒子的力,但不要改变其当前状态。在为每个粒子计算出它之后,然后根据之前的计算更新其内部状态。通过这种方式,即使单个线程也足够了,当然你可以在多个线程之间拆分计算,但是你需要额外的同步

JAVA 8 UPDATE

使用Java 8,您可以利用多核系统,而无需处理线程,同步等。

 while(true){
       allParticles.parallelStream().forEach(p -> {
           double force = p.calculateNetForce(allParticles);
           p.setNextPosition(force)
       });

       allParticles.parallelStream().forEach(p ->   p.nextState());      
 }

答案 4 :(得分:1)

对于每个粒子,你调用calculateNetForce(allTheParticles),我认为这使你的计算与O(N ^ 2)(所有粒子数的平方)成正比。这是主要的性能杀手,你最好找到一个复杂度为O(N)的算法,然后才尝试并行化。在我的头顶,我可以建议首先计算所有粒子的总质量和重心。然后,对于每个粒子,计算质量和其余粒子的中心。这可以通过获取总质量和中心,并添加具有负质量而不是当前粒子的“孔”来完成。然后计算粒子与其余粒子之间的力。每个粒子的计算是独立的,可以用其他评论者提出的任何方法进行并行化。

答案 5 :(得分:0)

粒子本身应该是Runnable和Callable,这样可以避免创建大量额外的对象并同步不同的步骤。这是一个SSCCEE:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Particle implements Callable<Void> {

  private enum ParticleState {
    POSITION_UPDATED, FORCE_CALCULATED
  }

  private int id;
  private int calculatedForce;
  private ParticleState particleState = ParticleState.POSITION_UPDATED;
  private List<Particle> allTheParticles;

  public Particle(int id, List<Particle> allTheParticles) {
    this.id = id;
    this.allTheParticles = allTheParticles;
  }

  private void calculateNetForce() {
    System.out.println("calculation in " + id);
    String someIntenseOperation = "";
    for (int i = 0; i < 10000; i++) {
      someIntenseOperation += allTheParticles.size();
    }
    calculatedForce = 0;
    particleState = ParticleState.FORCE_CALCULATED;
  }

  private void updatePosition() {
    System.out.println("updating position of " + id);
    particleState = ParticleState.POSITION_UPDATED;
  }

  @Override
  public Void call() throws Exception {
    switch (particleState) {
      case FORCE_CALCULATED:
        updatePosition();
        break;
      case POSITION_UPDATED:
        calculateNetForce();
        break;
    }
    return null;
  }

  public static void main(String[] args) throws InterruptedException {
    final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);
    final List<Particle> allTheParticles = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      allTheParticles.add(new Particle(i, allTheParticles));
    }
    while (true) {
      executor.invokeAll(allTheParticles);
      executor.invokeAll(allTheParticles);
    }
  }
}

答案 6 :(得分:0)

因为验证碰撞通常需要n ^ 2次计算,所以划分空间是一个好主意。虽然,它基本上是一个O(n ^ 2)问题。

这个问题声称矩阵逼近(但请查看Parallel computing以了解处理它的最佳想法) 您可以使用此处指出的一些技巧: An efficient way to simulate many particle collisions?

请注意,使用Actor model应该是一个坏主意,因为线程在一定数量后会出现问题。

现在有Java OpenCL lib(例如:Aparapi)和Java 9应该在Sumatra项目中本地带来openCL。因此,您可以使用Fork和Join lib,JVM将使用OpenCL。