如何在 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 的建议,否则我永远不会解决这个问题。
我正在 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 的建议,否则我永远不会解决这个问题。