Corrupted results in a multithreaded (i.e. threadpool-based) Java application

Viewed 112

I am experimenting with multithreading in Java, more specifically, threadpools. As a test, I have written an application that simply changes the color of an image using multithreading for speed. However, for some reason unknown to me, I get corrupted results depending on how I set up this test. Below I describe how the test application works together with the complete source code.

Any help is very welcome! Thank you!

The Test Application

I have a 400x300 pixel image buffer that is initialized with the dark blue color, as shown below:

enter image description here

The program must fill it up completely with the red color.

Although I could simply loop over all pixels, coloring each one sequentially with red, I've decided, for performance, to take advantage of parallelism. Thus, I've decided to fill each image row with a separate thread. Since the number of rows (300 rows) is much larger then the number of available CPU cores, I've created a threadpool (containing 4 threads) that will consume 300 tasks (each one in charge of filling up one row).

The program is organized as follows:

  • RGB class: holds the pixel color in a 3-tuple of doubles.
  • RenderTask class: fills up a given row of the image buffer with the red color.
  • Renderer class:
    • creates the image buffer.
    • creates the threadpool with "newFixedThreadPool".
    • creates 300 tasks to be consumed by the threadpool.
    • finishes the threadpool service.
    • writes the image buffer to a PPM file.

Below you may find the complete source code (I will call this code 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);
        }
    }
}

Everything seems to be working with that code, and I get the expected red image buffer, as shown below:

enter image description here

The Problem

However, if I modify the RenderTask.run() method such that it re-sets redundantly the color of the same buffer position several times in sequence, as shown below (I will call this one 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);

            }
        }
    }

Then I get the following corrupted image buffer:

enter image description here

Actually, the result is different each time I run the program, but always corrupted.

As far as I understand it, no two threads are writing to the same memory position simultaneously, so it seems that there is no racing condition in sight.

Even in the case of "false sharing", which I don't think is happening, I would expect only lower performance, not corrupted results.

Thus, even with redundant assignments, I would expect to get the correct result (i.e. a completely red image buffer).

So, my questions are: Why is this happening to the Version 2 of the program if the only difference with respect to Version 1 is that the assignment operation is being executed redundantly within the scope of the thread?

Would it be the case that some threads are being destroyed before they finish? Would it be a bug in the JVM? Or have I missed something trivial? (the strongest hypothesis :)

Thank you guys!!

2 Answers

ExecutorService.shutdown() does not await termination of the tasks it has, it only stops accepting new tasks.

After you have called shutdown you should call awaitTermination on the executor service if you want to wait for it to finish.

So what is happening is that all the tasks has not yet finished executing when you are starting to write the image to file.

@emil is correct. To add on to the answer, you can use the following code to close your threadpool

The following method shuts down an ExecutorService in two phases, first by calling shutdown to reject incoming tasks, and then calling shutdownNow, if necessary, to cancel any lingering tasks:

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();
  }
}

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

Related