如何在 Java 光线追踪器中实现多线程

How to implement multithreading in a Java ray tracer

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

我的解决方案目前有效,但当更多线程并行工作时,渲染时间不会减少。我的CPU有8个线程,在8个线程上渲染时使用率不是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%。 我不指望任何人能解决我的问题,但在实现多线程方面,我真的很感激一些指导。我是不是用错了方法?

在您发布的源代码中,我没有看到明显的瓶颈。当并行代码变慢 运行 时,最常见的解释要么是因为同步导致的开销,要么是做额外的工作。

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

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

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

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

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

 kill -3 JAVA_PID

程序(或 运行s 它的 JVM)然后将打印所有当前线程的当前堆栈跟踪。如果你重复几次,你应该知道你的程序大部分时间都花在哪里了。

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

希望对入门有所帮助。

我解决了这个问题,我终于明白为什么它不起作用了。

通过使用 VisualVM 进行一些调试,我注意到除了一个线程之外的所有线程始终被阻塞。我最初的解决方法是复制传递给每个线程的 Scene 对象。它解决了这个问题,但它并不优雅,对我来说没有意义。事实证明,真正的解决方案要简单得多。

我在我的场景 class 中使用 Vector<> 作为几何体的容器。 Vector<> 是一个同步容器,不允许多个线程同时访问它。 通过将场景中的所有对象放在 ArrayList<> 中,我得到了更简洁的代码,更少的内存使用和更好的性能。

VisualVM 是找到阻塞的关键,我感谢 Philipp Claßen 的建议,否则我永远不会解决这个问题。