【问题标题】:Corrupted results in a multithreaded (i.e. threadpool-based) Java application多线程(即基于线程池的)Java 应用程序中的损坏结果
【发布时间】:2020-02-04 08:46:44
【问题描述】:

我正在试验 Java 中的多线程,更具体地说,是线程池。作为测试,我编写了一个应用程序,它使用多线程来简单地更改图像的颜色以提高速度。但是,由于某些我不知道的原因,我会根据我设置此测试的方式得到损坏的结果。下面我将描述测试应用程序如何与完整的源代码一起工作。

非常欢迎任何帮助!谢谢!

测试应用程序

我有一个 400x300 像素的图像缓冲区,初始化为深蓝色,如下所示:

程序必须用红色完全填满它。

虽然我可以简单地循环遍历所有像素,用红色依次为每个像素着色,但出于性能考虑,我决定利用并行性。因此,我决定用一个单独的线程填充每个图像行。由于行数(300 行)远大于可用 CPU 内核的数量,因此我创建了一个线程池(包含 4 个线程),它将消耗 300 个任务(每个任务负责填满一行)。

节目安排如下:

  • RGB 类:将像素颜色保存在双精度的 3 元组中。
  • RenderTask 类:用红色填充图像缓冲区的给定行。
  • 渲染器类:
    • 创建图像缓冲区。
    • 使用“newFixedThreadPool”创建线程池。
    • 创建 300 个任务供线程池使用。
    • 完成线程池服务。
    • 将图像缓冲区写入 PPM 文件。

您可以在下面找到完整的源代码(我将此代码称为Version 1):

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.io.*;

class RGB {
    RGB() {}

    RGB(double r, double g, double b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    double r;
    double g;
    double b;
}

class RenderTask implements Runnable {
    RenderTask(RGB[][] image_buffer, int row_width, int current_row) {
        this.image_buffer = image_buffer;       
        this.row_width = row_width;
        this.current_row = current_row; 
    }

    @Override
    public void run() {   
        for(int column = 0; column < row_width; ++column) {
            image_buffer[current_row][column] =  new RGB(1.0, 0.0, 0.0);
        }
    }

    RGB[][] image_buffer;
    int row_width;
    int current_row;
}

public class Renderer {
    public static void main(String[] str) {
        int image_width = 400;
        int image_height = 300;

        // Creates a 400x300 pixel image buffer, where each pixel is RGB triple of doubles,
        // and initializes the image buffer with a dark blue color.
        RGB[][] image_buffer = new RGB[image_height][image_width];
        for(int row = 0; row < image_height; ++row)
            for(int column = 0; column < image_width; ++column)
                image_buffer[row][column] = new RGB(0.0, 0.0, 0.2); // dark blue        

        // Creates a threadpool containing four threads
        ExecutorService executor_service = Executors.newFixedThreadPool(4);

        // Creates 300 tasks to be consumed by the threadpool:
        //     Each task will be in charge of filling one line of the image buffer.
        for(int row = 0; row < image_height; ++row)
            executor_service.submit(new RenderTask(image_buffer, image_width, row));

        executor_service.shutdown();

        // Saves the image buffer to a PPM file in ASCII format
        try (FileWriter fwriter = new FileWriter("image.ppm");
            BufferedWriter bwriter = new BufferedWriter(fwriter)) {

            bwriter.write("P3\n" + image_width + " " + image_height + "\n" + 255 + "\n");

            for(int row = 0; row < image_height; ++row)
                for(int column = 0; column < image_width; ++column) {
                    int r = (int) (image_buffer[row][column].r * 255.0);
                    int g = (int) (image_buffer[row][column].g * 255.0);
                    int b = (int) (image_buffer[row][column].b * 255.0);
                    bwriter.write(r + " " + g + " " + b + " ");
                }                
        } catch (IOException e) {
            System.err.format("IOException: %s%n", e);
        }
    }
}

一切似乎都在使用该代码,我得到了预期的红色图像缓冲区,如下所示:

问题

但是,如果我修改 RenderTask.run() 方法,使其按顺序重复多次重新设置同一缓冲区位置的颜色,如下所示(我将其称为 Version 2强>):

    @Override
    public void run() {   
        for(int column = 0; column < row_width; ++column) {
            for(int s = 0; s < 256; ++s) {

                image_buffer[current_row][column] =  new RGB(1.0, 0.0, 0.0);

            }
        }
    }

然后我得到以下损坏的图像缓冲区:

其实每次运行程序的结果都不一样,但总是损坏。

据我了解,没有两个线程同时写入同一个内存位置,所以看起来没有竞争条件。

即使在“错误共享”的情况下(我认为不会发生这种情况),我预计只会降低性能,而不是损坏结果。

因此,即使有多余的分配,我也希望得到正确的结果(即完全红色的图像缓冲区)。

所以,我的问题是:如果与版本 1 的唯一区别是在线程范围内冗余执行赋值操作,为什么程序版本 2 会发生这种情况?

会不会是某些线程在完成之前就被销毁了?这会是JVM中的错误吗? 还是我错过了一些微不足道的事情? (最强假设:)

谢谢你们!!

【问题讨论】:

  • s 应该代表什么?您是否忘记将image_buffer 索引为s 而不是current_row?
  • 为什么这么多任务?生成和使用任务会产生开销,因此您最好生成与线程一样多的任务,并允许每个线程渲染大约 1/n 行或像素。

标签: java multithreading threadpool


【解决方案1】:

@emil 是正确的。要补充答案,您可以使用以下代码关闭线程池

以下方法分两个阶段关闭一个ExecutorService,首先调用shutdown拒绝传入的任务,然后在必要时调用shutdownNow取消任何延迟的任务:

void shutdownAndAwaitTermination(ExecutorService pool) {
  pool.shutdown(); // Disable new tasks from being submitted
  try {
    // Wait a while for existing tasks to terminate
    if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
      pool.shutdownNow(); // Cancel currently executing tasks
      // Wait a while for tasks to respond to being cancelled
      if (!pool.awaitTermination(60, TimeUnit.SECONDS))
          System.err.println("Pool did not terminate");
    }
  } catch (InterruptedException ie) {
    // (Re-)Cancel if current thread also interrupted
    pool.shutdownNow();
    // Preserve interrupt status
    Thread.currentThread().interrupt();
  }
}

来源:https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/concurrent/ExecutorService.html

【讨论】:

    【解决方案2】:

    ExecutorService.shutdown() 不会等待它拥有的任务终止,它只会停止接受新任务。

    调用shutdown后,如果你想等待它完成,你应该调用executor服务上的awaitTermination。

    所以发生的情况是,当您开始将图像写入文件时,所有任务尚未完成执行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-05-23
      • 1970-01-01
      • 1970-01-01
      • 2012-06-16
      • 1970-01-01
      • 2012-08-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多