如何在Java ray跟踪器中实现多线程

时间:2019-06-18 00:22:26

标签: java multithreading performance runnable

我正在用Java编写光线跟踪程序,并已使用Runnable接口实现了多线程。每个线程呈现800条垂直线的一部分。当使用两个线程时,它们将分别渲染400行。对于8个线程,每个线程100条,依此类推。

我的解决方案当前正在工作,但是使用更多线程时,渲染所花费的时间会增加。当线程数加倍时,渲染每行的时间增加大约3倍。但是更多的线同时进行渲染,因此总渲染时间增加了大约50%,这仍然很糟糕。它应该更快。

我知道线程只能渲染正确的线,我已经检查过相同的像素不会被渲染多次。 我试图确保每个线程在渲染循环中都写入自己的变量,并在整个循环完成后对它们进行求和/合并。 我的CPU有8个线程,即使在8个线程上渲染,CPU使用率也很高,但不是100%。

class Multithread implements Runnable {
  Camera camera;
  CountDownLatch latch;
  ...

  //Constructor for thread
  Multithread(Scene s, Camera c, int thread, int threadcount, CountDownLatch cdl){
      camera = c;
      latch = cdl;
      ...
  }

  public void run(){
      try{
          ...
          //This is the render function
          camera.render(...);

          //When all threads unlatch, main class will write PNG
          latch.countDown();
      }
      catch (Exception e){System.out.println ("Exception is caught");}
  }
}
public class Camera {
    //The final pixel values are stored in the 2D-array
    ColorDbl[][] finalImage;

    Camera(int w){
        Width = w;
        finalImage = new ColorDbl[w][w]
    }

    //Start rendering
    void render(Scene S, int start, int end){

        //Create temporary, partial image
        ColorDbl[][] tempImage = new ColorDbl[Width][Width];

        Ray r;
        ColorDbl temp;
        //Render lines of pixels in the interval start-end
        for(int j = start; j < end; ++j){
            for(int i = 0; i < Width; ++i){
                r = new Ray(...);
                temp = r.CastRay(...);
                tempImage[i][j] = temp;
            }
        }

        //Copy rendered lines to final image
        for(int j=start; j<end; ++j){
            for(int i=0; i<Width; ++i){
                finalImage[i][j] = tempImage[i][j];
            }
        }
    }

    public static void main(String[] args) throws IOException{
        //Create camera and scene
        Camera camera = new Camera(800);
        Scene scene = new Scene();

        //Create threads
        int threadcount = 4;
        CountDownLatch latch = new CountDownLatch(threadcount);
        for (int thread=0; thread<threadcount; thread++){
            new Thread(new Multithread(scene, camera, thread, threadcount, latch)).start();
        }

        //Wait for threads to finish
        try{
          latch.await();
        }catch(InterruptedException e){System.out.println ("Exception");}

        //Write PNG
        c.write(...);
    }
}

当使用2个线程而不是1个线程时,我期望渲染速度几乎翻倍,但它需要的时间要长50%。 我不希望有人能解决我的问题,但是在实现多线程方面,我真的很感谢一些指导。我会用这种错误的方式吗?

2 个答案:

答案 0 :(得分:0)

在您发布的源代码中,我看不到明显的瓶颈。当并行代码运行速度较慢时,最常见的解释要么是由于同步引起的开销,要么是额外的工作。

当涉及到同步时,高度拥塞会使并行代码运行非常缓慢。这可能意味着线程(或进程)正在争夺有限的资源(例如,等待锁),但也可能更加微妙,例如使用原子操作访问相同的内存,这可能会变得非常昂贵。在您的示例中,我没有看到任何类似的东西。唯一的同步操作似乎是最后的倒数锁存器,这并不重要。不平等的工作量也会损害可伸缩性,但是在您的示例中似乎不太可能。

做额外的工作可能是一个问题。也许您在并行版本中复制的数据比在顺序版本中复制的数据更多?这可以解释一些开销。另一个猜测是在并行版本中,缓存局部性受到了负面影响。请注意,缓存的作用非常明显(根据经验,当您的工作负载不再适合缓存时,内存访问可能会慢50-100倍)。

如何找到瓶颈?通常,这称为分析。有专门的工具,例如VisualVM是Java的免费工具,可以用作探查器。另一种更简单但通常非常有效的第一种方法是运行程序并进行一些随机线程转储。如果您有明显的瓶颈,则很有可能会在堆栈跟踪中看到它。

该技术通常被称为可怜人的剖析器,但我发现它非常有效(有关更多详细信息,请参见this answer)。此外,您还可以在生产环境中安全地应用它,因此,当您不得不优化无法在本地计算机上运行的代码时,这是一个巧妙的技巧。

IDE(例如Eclipse或IntelliJ)支持进行线程转储,但是如果您知道进程ID,也可以直接从命令行触发它:

 kill -3 JAVA_PID

然后,程序(或运行它的JVM)将打印所有当前线程的当前堆栈跟踪。如果重复几次,您应该了解程序在大部分时间上的花费。

您还可以将其与顺序版本进行比较。也许您会注意到某种解释并行版本开销的模式。

希望这对您有所帮助。

答案 1 :(得分:0)

我找到了问题并解决了,现在它可以完美地与16个线程一起工作,几乎比1个线程快16倍。

我认为问题在于创建每个线程时,我传递了相同的Scene对象,而不是新的Scene对象。因此,在渲染过程中,线程必须在其他函数中互相等待。

//This is what caused the blocking
Scene s1 = new Scene(arg, arg);
Scene s2 = s1;
Scene s3 = s1;

Thread T1 = new Thread(s1);
Thread T2 = new Thread(s2);
Thread T3 = new Thread(s3);

T1.start();
T2.start();
T3.start();

解决方法:

Scene s1 = new Scene(arg, arg);
Scene s2 = new Scene(arg, arg);
Scene s3 = new Scene(arg, arg);
...

VisualVM对于找到阻塞非常关键,我感谢Philipp Claßen的建议,因为我永远不会解决这个问题。