Home > OS >  Why is my ExecutorService implementation performing worse than single threaded implementation?
Why is my ExecutorService implementation performing worse than single threaded implementation?

Time:06-18

I've been attempting to multi-thread Conway's game of life program. Part of my naive solution was to do roughly what I've done below. However, even this basic implementation performs significantly more poorly than a single-threaded solution, highlighting the fact that I'm not understanding something.

I should also say, in case it isn't clear, I need to 'join' the threads at the end of each iteration of the while loop, which is why I have used Callable - so that I can obtain and iterate through the Futures.

Any feedback, advice suggestions would be wonderfully well received.

package lewi0231.testOne;

import java.util.ArrayList;
import java.util.concurrent.*;

public class LongRunningTask {
    int count;
    boolean[][] board = new boolean[200][200];
    ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public LongRunningTask(int count) {
        this.count = count;
    }

    public void processTask() {
        int i = 0;
        ArrayList<Future<Integer>> futures = new ArrayList<>();

        while(i < count){
            for (int k = 1; k < board.length; k  ){
                futures.add(threadPool.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        for (int l = 1; l < board[0].length; l  ){
                                //Do Something Here
                        }
                        return 1;
                    }
                }));
            }

            for (Future<Integer> f : futures){
                try {
                    f.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }

            i  ;
        }
    }
}

class HypotheticalGUI{
    public static void main(String[] args) {
        LongRunningTask task = new LongRunningTask(100);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                task.processTask();
            }
        });
        thread.start();
    }
}

CodePudding user response:

When the number of threads (running) significantly outweighs the number of available processors, excessive context switching can occur. This means that the overhead of creating the thread plus that of switching between threads impacts performance.

Context switching is particularly disruptive because whenever a processor switches from one thread to another it must flush its cache and reload the new thread's context. Context in this sense refers to a thread's stack, register and program counter content.

A better approach would be to attempt to match the number of available processors with the number of threads created. See below:

import java.util.ArrayList;
import java.util.concurrent.*;

public class LongRunningTask {
    int count;
    boolean[][] board = new boolean[200][200];
    int numProcessors = Runtime.getRuntime().availableProcessors() - 1;
    ExecutorService threadPool = Executors.newFixedThreadPool(numProcessors);

    public LongRunningTask(int count) {
        this.count = count;
    }

    public void processTask() {
        int i = 0;
        ArrayList<Future<Integer>> futures = new ArrayList<>();
        int startIndex = 1;
        int partitionSize = board.length / numProcessors;

        while(i < count){
            while (startIndex < board.length) {
                futures.add(threadPool.submit(new MyCallable(startIndex, partitionSize, board)));
                startIndex  = partitionSize;
            }

            for (Future<Integer> f : futures){
                try {
                    f.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }

            i  ;
        }
    }
}

class MyCallable implements Callable<Integer> {
    private final int startIndex;
    private final int partitionSize;
    private final boolean[][] board;

    public MyCallable(int startIndex, int partitionSize, boolean[][] board){
        this.startIndex = startIndex;
        this.partitionSize = partitionSize;
        this.board = board;
    }

    @Override
    public Integer call() throws Exception {
        int endIndex = Math.min(startIndex   partitionSize, board.length - 1);
        for (int k = startIndex; k < endIndex; k  ){
            for (int l = 1; l < board[0].length; l  ){
                //Do Something Here
            }
        }
        return 1;
    }
}

class HypotheticalGUI{
    public static void main(String[] args) {
        LongRunningTask task = new LongRunningTask(100);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                task.processTask();
            }
        });
        thread.start();
    }
}
  • Related