Site Search:

CellularAutomata.java

<Back

CellularAutomata.java added a CyclicBarrier and worker threads to print the same result as GameOfLife.java no GUI version, with better performance from better concurrency.


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CellularAutomata {
    /**
     * Monitor of the data model for mainBoard and its subBoards
     * */
    private final Board mainBoard;
    /**
     * The boardGrid is partitioned into Ncpu parts, and assign each part to a thread. 
     * At each step, the worker threads calculate new values for all the cells in their part of the board with 
     * a reference to the global board.
     * When all worker threads have reached the barrier, one of the thread run the barrier action to combines/commits 
     * the subBoards' new values to the boardGrid.
     * After the barrier action runs, the worker threads are released to compute the next step of the calculation.
     * */
    private final CyclicBarrier barrier;
    private final Worker[] workers;
    public static final int TOTALGENERATIONS = 25; 
    
    /**
     * boardGrid is shared among worker threads, however only the one running the barrier action should update the
     * value of boardGrid. 
     * The worker threads should always treat this array as read only unless it is running the barrier action. 
     * 
     * Java memory models consider each array element as a separate variable.
     * 
     * JVM memoery model guarantee initialization safety with static or final keyword.
     *
     * Happens-before relationship is established by the barrier.await() -- when a thread is running the barrier action,
     * all data model before barrier.await() is happens-before to that thread; 
     * when the thread finish running the barrier action, the data model modified is happens-before for next threads calling
     * barrier.await().
     * 
     * We don't need update atomicity, only one thread is update boardGrid in barrier action, while the
     * updating is happening, the worker threads are not reading it.
     * **/
    static final int[][] boardGrid = new int[10][10];  //default 0
    
    public static void main(String...args) {
        //initialBoard[0][1] = initialBoard[0][2] = initialBoard[0][3] = initialBoard[0][4] = 1;  //seeds
        boardGrid[0][0] = boardGrid[0][1] = boardGrid[0][2] = boardGrid[0][3] = boardGrid[0][4] = 1;  //seeds
        
        CellularAutomata cells = new CellularAutomata(boardGrid);
        cells.start();
    }

    public CellularAutomata(int[][] boardGrid) {
        visualizeBoard(boardGrid);
        this.mainBoard = new GameBoard(boardGrid);
        int count = Runtime.getRuntime().availableProcessors();
        System.out.println("total processor: " + count);
        this.barrier = new CyclicBarrier(count,
                new Runnable() {
                    public void run() {
                        mainBoard.commitNewValues();
                    }});
        this.workers = new Worker[count];
        for (int i = 0; i < count; i++)
            workers[i] = new Worker(boardGrid, mainBoard.getSubBoard(count, i));
    }
    
    //suppose to be called by a single thread (main thread, or swing event dispatch thread later)
    public final static void visualizeBoard(int[][] board) {
        for (int row[] : board) {
            for (int cell : row) {
                System.out.print(cell == 1 ? "#" : ".");
            }
            System.out.println();
        }
        System.out.println();
    }

    private class Worker implements Runnable {
        private final int[][] mainBoardView//suppose to be used as an unmodifiable view of the latest mainBoard
        private final Board board;

        public Worker(int[][] mainBoardView, Board board) { 
            this.mainBoardView = mainBoardView;
            this.board = board
        }
        public void run() {
            while (!board.hasConverged()) {
                for (int x = 0; x < board.getMaxX(); x++)
                    for (int y = 0; y < board.getMaxY(); y++)
                        board.setNewValue(x, y, computeValue(x, y));
                System.out.println(Thread.currentThread().getName() + " finished one turn");
                try {
                    barrier.await();
                } catch (InterruptedException ex) {
                    return;
                } catch (BrokenBarrierException ex) {
                    return;
                }
            }
        }

        private int computeValue(int x, int y) {
            // Compute the new value that goes in (x,y)
            return nextCellState(mainBoardView, x, y);
        }
        
        private final int nextCellState(int[][] board, int i, int j) {
            int count = countSurrounding(board, i, j);
            if (board[i][j] == 1 && (count == 2 || count == 3)) { // stay alive rule
                return 1;
            }
            if (board[i][j] == 0 && count == 3) { // birth new rule
                return 1;
            }
            return 0;
        }
        
        private final int countSurrounding(int[][] board, int a, int b) {
            int count = 0;
            int[][] surroundings = { { a - 1, b - 1 }, { a - 1, b },
                    { a - 1, b + 1 }, { a, b - 1 }, { a, b + 1 }, { a + 1, b - 1 },
                    { a + 1, b }, { a + 1, b + 1 } };
            for (int surrounding[] : surroundings) {
                try {
                    if (board[surrounding[0]][surrounding[1]] == 1) {
                        count++;
                    }
                } catch (ArrayIndexOutOfBoundsException e) {
                }
            }
            return count;
        }
    }

    public void start() {
        for (int i = 0; i < workers.length; i++)
            new Thread(workers[i]).start();
        mainBoard.waitForConvergence();
    }

    interface Board {
        int getMaxX();
        int getMaxY();
        int getValue(int x, int y);
        int setNewValue(int x, int y, int value);
        void commitNewValues();
        boolean hasConverged();
        void waitForConvergence();
        Board getSubBoard(int numPartitions, int index);
    }
    
    /** mainBoard stores the global data model and the data models of all the subBoards
     *  A subBoard only stores the data model of its own
     *  
     *  Of course, in elastic computation environment, a subBoard can further be spit into 2nd level subBoards and acts
     *  as 2nd level mainBoard.
     *  **/
    final class GameBoard implements Board{
        private final int[][] board;
        /**
         * the mainBoard have a non-empty map, which stores the data models of all the subBoards
         * mainBoard retains a reference to the data model for each of the subBoards, 
         * so that they can be combined to update the mainBoard's data model when all threads reaches the barrier.
         * The data model of the subBoard are not really accessed concurrently at any moment. 
         * Indexed subBoard is one per worker thread.
         * After barrier.await() reaches, only one of the threads can read these data models in the barrier action. 
         * -- only after all worker threads have finished modifying their own data models and happens-before relationship
         * established by barrier.await().
         * **/
        private final Map<Integer, int[][]> subBoards = new HashMap<>();
        private volatile int nthGeneration = 0;
        
        public GameBoard(int[][] mainboardView) {
            this.board = mainboardView;
        }

        @Override
        public int getMaxX() {
            return this.board.length;
        }

        @Override
        public int getMaxY() {
            return this.board[0].length;
        }

        @Override
        public int getValue(int x, int y) {
            return this.board[x][y];
        }

        @Override
        /**
         * subBoard calls this method to update its own data model, the mainBoard's data model remain unchanged,
         * however, mainBoard kept a reference to the subBoard's data model, so these update is visible to the mainBoard.
         * */
        public int setNewValue(int x, int y, int value) {
            this.board[x][y] = value;
            return this.board[x][y];
        }

        @Override
        /**only mainBoard should call this method. 
         * The data models of the subBoards are combined to update the data model of the mainBoard*/
        public void commitNewValues() {
            for(int index : subBoards.keySet()) {
                int rowsCopied = 0;
                int[][] subBoard = subBoards.get(index);
                for (int i = 0; i < subBoard.length; i++) {
                    for (int j = 0; j < subBoard[0].length; j++) {
                        board[rowsCopied + i][j] = subBoard[i][j];
                    }
                }
                rowsCopied += subBoard.length;
            }
            
            System.out.println("\nGeneration " + nthGeneration++);
            CellularAutomata.visualizeBoard(board);
        }
        
        @Override
        /**
         * subBoard can not tell when the game converged without global knowledge of the mainBoard.
         * So it assuming converge after TOTALGENERATIONS.
         * */
        public boolean hasConverged() {
            if(nthGeneration++ <= CellularAutomata.TOTALGENERATIONS) {
                return false;
            }
            return true;
        }

        @Override
        /**
         * only mainBoard should call this method. 
         * one way to tell convergence is to 
         * */
        public void waitForConvergence() {
            while(nthGeneration<=25) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println("wait for convergence");
            }
        }

        @Override
        /**
         * only mainBoard should call this method. It create a new array as the data model for the subBoard.
         * mainBoard retains a reference to the data model of the subBoard.
         * the data model of the subBoard are not suppose to be accessed concurrently. 
         * Indexed subBoard is one per worker thread.
         * After barrier.await() reaches, only one of the threads can read these data models in the barrier action. 
         * -- only after all worker threads have finished modifying their own data models and happens-before relationship
         * established by barrier.await().
         * */
        public Board getSubBoard(int numPartitions, int index) {
            int count = board.length;
            int rows = count / numPartitions;
            int residue = count % numPartitions;
            int[][] subBoard = null;
            if(index == 0) {
                subBoard = new int[rows + residue][board[0].length];
            } else {
                subBoard = new int[rows][board[0].length]; 
            }
            this.subBoards.put(index, subBoard);
            return new GameBoard(subBoard);
        }
    }

}