Поврежденные результаты в многопоточном (т.е. основанном на пуле) Java-приложении - PullRequest
3 голосов
/ 07 октября 2019

Я экспериментирую с многопоточностью в Java, точнее, с потоками. В качестве теста я написал приложение, которое просто меняет цвет изображения, используя многопоточность для скорости. Однако по неизвестной мне причине я получаю искаженные результаты в зависимости от того, как я настроил этот тест. Ниже я опишу, как тестовое приложение работает вместе с полным исходным кодом.

Любая помощь очень приветствуется! Спасибо!

Тестовое приложение

У меня есть буфер изображения размером 400x300 пикселей, инициализированный синим цветом, как показано ниже:

enter image description here

Программа должна полностью заполнить его красным цветом.

Хотя я мог бы просто зациклить все пиксели, покрасив каждый последовательно красным, яДля производительности решил воспользоваться преимуществами параллелизма. Таким образом, я решил заполнить каждую строку изображения отдельным потоком. Поскольку количество строк (300 строк) намного превышает количество доступных ядер ЦП, я создал пул потоков (содержащий 4 потока), который будет выполнять 300 задач (каждая из которых отвечает за заполнение одной строки).

Программа организована следующим образом:

  • Класс RGB: содержит цвет пикселя в трех кортежах.
  • Класс RenderTask: заполняет данную строкубуфер изображения с красным цветом.
  • Класс рендерера:
    • создает буфер изображения.
    • создает пул потоков с помощью "newFixedThreadPool".
    • создает 300 задач, которые будут использоваться пулом потоков.
    • завершает службу пула потоков.
    • записывает буфер изображения в файл PPM.

Ниже вы можете найти полный исходный код (Я назову этот код Версия 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);
        }
    }
}

Кажется, все работает с этим кодом, и я получаю ожидаемый красный буфер изображения, как показано ниже:

enter image description here

Проблема

Однако, если я изменю метод RenderTask.run () так, чтобы он переустанавливался с избыточностьюцвет одной и той же позиции буфера несколько раз в последовательности, как показано ниже (я назову это Версия 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);

            }
        }
    }

Затем я получаю следующий поврежденный буфер изображения:

enter image description here

На самом деле, результат меняется каждый раз, когда я запускаю программу, новсегда поврежден.

Насколько я понимаю, нет двух потоков, которые одновременно пишут в одну и ту же позицию памяти, поэтому кажется, что в поле зрения нет гоночных условий.

Даже в случае «ложного обмена», который, как я думаю, не происходит, я ожидал бы только более низкую производительность, а не искаженные результаты.

Таким образом, даже при избыточных назначениях яожидал получить правильный результат (т.е. полностью красный буфер изображения).

Итак, мои вопросы: почему это происходит с версией 2 программы, если единственное отличие от версии 1 состоит в том, что операция присваивания выполняется избыточно в рамках потока?

Было бы так, что некоторые потоки уничтожаются до того, как они закончат работу? Это будет ошибка в JVM? Или я пропустил что-то тривиальное? (самая сильная гипотеза:)

Спасибо, ребята !!

Ответы [ 2 ]

5 голосов
/ 07 октября 2019

ExecutorService.shutdown () не ожидает завершения своих задач, он только прекращает принимать новые задачи.

После того, как вы вызвали shutdown, вы должны вызвать awaitTermination в службе executor, если вы хотите ждатьэто до конца.

Так что происходит, что все задачи еще не завершены, когда вы начинаете записывать изображение в файл.

3 голосов
/ 07 октября 2019

@ Эмиль правильно. Чтобы добавить ответ, вы можете использовать следующий код для закрытия пула потоков

Следующий метод завершает работу 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

...