Игра жизни Конвея, в которой каждая клетка является нитью

Игра жизни Конвея, в которой каждая клетка является нитью

Привет, ребята, Итак, как следует из названия, я должен написать программу, реализующую многопоточность в игре жизни Конвея, где каждая мертвая или живая клетка является потоком. Моя первая цель состояла в том, чтобы просто заставить игру работать, что я и сделал (довольно забавная задача). Итак, я могу распечатать сетку 20x20, и каждая ячейка инициализируется случайным числом 1 или 0, где 1 живой, а 0 мертвый. Теперь я смотрел видео о том, как использовать многопоточность, но я все еще не уверен, как я должен реализовать это для каждой ячейки... У меня есть 3 класса: Main, Cell и CellRules мой класс Cell выглядит как:

    public class Cell implements Runnable 
    {
        public static int myCount = 0;
        private String name;
        private int neighbors;
        private int alive; // 0 is dead; 1 is alive.

        Random rand = new Random();


        public Cell (String nm)
        {
            name = nm;
            myCount = rand.nextInt(999);
            neighbors = 0;

            // 2 because it is exclusive
            this.setLife(rand.nextInt(2));
        }

            public void run()
        {
            while(Cell.myCount <= 10){
                try
                {
                    System.out.println("Expl Thread: " + (++Cell.myCount));
                    Thread.sleep(100);
                } catch (InterruptedException iex) 
                {
                    System.out.println("Exception in thread: 
                    "+iex.getMessage());
                }
            }
        }

Там есть еще несколько вещей, которые действительно для простоты, я не думаю, что их нужно показывать, и то же самое для класса Cell Rules. Правила ячейки выглядят так:

    /**
     * This function simply gets the number of neighbors for each cell, and saves the future generation
     * based on the number neighbors from arr to future array.
     * 
     * @param arr Arr that will be checked for neighbors
     * @param futureGen This array will keep track of the future generation
     * @param columns numbers of columns
     * @param rows numbers of rows
     */
        public void checkN(Cell [][] arr, Cell [][] futureGen, int columns, int rows)
        {
            for (int x = 0; x < rows; x++) 
            {
                for (int y = 0; y < columns; y++) 
                {               
                    arr[x][y].setNeighbors(0);

                    // Upper left corner
                    if (x == 0 && y == 0)
                        for (int i = 0; i <= 1; i++)
                            for (int j = 0; j <= 1; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Upper margin checks
                    if ((x == 0) && (y != 0 && y <= columns - 2))
                        for(int i = 0; i <= 1; i++)
                            for(int j = -1; j <= 1; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Upper right corner
                    if ((x == 0) && (y == columns - 1))
                        for(int i = 0; i <= 1; i++)
                            for(int j = -1; j <= 0; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Left margin checks
                    if ((y == 0) && (x != 0 && x <= rows - 2))
                        for (int i = -1; i <= 1; i++)
                            for (int j = 0; j <= 1; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();


                    // Lower left corner
                    if ((x == rows - 1) && y == 0)
                        for (int i = -1; i <= 0; i++)
                            for (int j = 0; j <= 1; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Bottom margin checks
                    if ((x == rows - 1) && (y != 0 && y <= columns - 2 ))
                        for (int i = -1; i <= 0; i++)
                            for (int j = -1; j <= 1; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Lower right corner
                    if ((x == rows - 1) && (y == columns - 1))
                        for (int i = -1; i <= 0; i++)
                            for (int j = -1; j <= 0; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Right margin checks
                    if ((y == columns - 1) && (x != 0 && x <= rows - 2))
                        for (int i = -1; i <= 1; i++)
                            for (int j = -1; j <= 0; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Middle area checks ( can check all around now )!
                    if ((x > 0) && (x < rows - 1) && (y > 0) && (y < columns - 1) )
                        for (int i = -1; i <= 1; i++)
                            for (int j = -1; j <= 1; j++)
                                if (arr[x + i][y + j].getLife() == 1)
                                    arr[x][y].addNeighbor();

                    // Do not add yourself!
                    if (arr[x][y].getLife() == 1)
                        arr[x][y].subNeighbor();

                    // Get the new generation through the neighbors
                    if ((arr[x][y].getLife() == 1) && 
                        (arr[x][y].getNeighbors() < 2))
                        futureGen[x][y].setLife(0);
                    else if ((arr[x][y].getLife() == 1) && 
                             (arr[x][y].getNeighbors() > 3))
                        futureGen[x][y].setLife(0);
                    else if ((arr[x][y].getLife() == 0) && 
                             (arr[x][y].getNeighbors() == 3))
                        futureGen[x][y].setLife(1);
                    else
                        futureGen[x][y].setLife(arr[x][y].getLife());               
                }
            }
        }

Я не уверен, куда на самом деле идет реализация потоков, любые рекомендации или объяснения будут очень признательны! Хорошего дня :)


person Nano    schedule 01.12.2017    source источник


Ответы (1)


Во-первых, ваша функция checkN сильно перегружена. Давайте немного упростим.

/**
 * This function simply gets the number of neighbors for each cell, and saves the future generation
 * based on the number neighbors from arr to future array.
 * 
 * @param arr Arr that will be checked for neighbors
 * @param futureGen This array will keep track of the future generation
 * @param columns numbers of columns
 * @param rows numbers of rows
 */
public void checkN(Cell [][] arr, Cell [][] futureGen, int columns, int rows) {
    for (int x = 0; x < rows; x++) {
        for (int y = 0; y < columns; y++) {               
            arr[x][y].setNeighbors(0);

            for(int i = -1; i <= 1; i++) {
                for(int j = -1; j <= 1; j++) {
                    if(i == 0 && j == 0) continue; //don't check self
                    if(x + i < 0 || x + i >= rows) continue; //bounds checking
                    if(y + j < 0 || y + j >= columns) continue; //bounds checking

                    if (arr[x + i][y + j].getLife() == 1)
                            arr[x][y].addNeighbor();
                }
            }

            // Get the new generation through the neighbors
            if(arr[x][y].getLife() == 1 &&
                (arr[x][y].getNeighbors() == 2 || arr[x][y].getNeighbors() == 3))
                futureGen[x][y].setLife(1);
            else if(arr[x][y].getLife() == 0 && arr[x][y].getNeighbors() == 3)
                futureGen[x][y].setLife(1);
            else
                futureGen[x][y].setLife(0);               
        }
    }
}

Затем мы можем преобразовать это в несколько дополнительных функций:

private void countNeighbors(Cell[][] arr, int row, int column, int rows, int columns) {
    Cell c = arr[row][column];
    c.setNeighbors(0);

    for(int i = -1; i <= 1; i++) {
        for(int j = -1; j <= 1; j++) {
            if(i == 0 && j == 0) continue; //don't check self
            if(row + i < 0 || row + i >= rows) continue; //bounds checking
            if(column + j < 0 || column + j >= columns) continue; //bounds checking

            if (arr[row + i][column + j].getLife() == 1)
                    c.addNeighbor();
        }
    }
}

private void evaluateNeighbors(Cell oldCell, Cell newCell) {
    if (oldCell.getLife() == 1 &&
        (oldCell.getNeighbors() == 2 || oldCell.getNeighbors() == 3))
        newCell.setLife(1);
    else if(oldCell.getLife() == 0 && oldCell.getNeighbors() == 3)
        newCell.setLife(1);
    else
        newCell.setLife(0);
}

public void checkN(Cell [][] arr, Cell [][] futureGen, int columns, int rows) {
    for (int row = 0; row < rows; row++) {
        for (int column = 0; column < columns; column++) {               
            countNeighbors(arr, row, column, rows, columns);

            evaluateNeighbors(arr[row][column], futureGen[row][column]);            
        }
    }
}

Причина, по которой мы проводим такой рефакторинг, станет очевидной через мгновение.

Итак, вернемся к исходному вопросу: где мы вставляем потоки в эту программу?

Вам нужно принять решение о том, как вы хотите разделить потоки. Судя по вашему вопросу, похоже, вы хотите запустить независимый поток для каждой оцениваемой ячейки. Хотя в теории с этим подходом нет никаких проблем, на самом деле он не гарантирует какого-либо значительного ускорения, просто потому, что при тысячах ячеек (которые вы быстро нажмете даже для скромных размеров сетки) вы будете создавать тысячи потоков и только у мегасерверов будет достаточно потоков, чтобы воспользоваться этим.

Тем не менее, если это то, что вы хотите сделать, код (требуется Java 8) выглядит следующим образом:

public void checkN(Cell [][] arr, Cell [][] futureGen, int columns, int rows)
{
    ArrayList<Thread> threads = new ArrayList<>();
    for (int row = 0; row < rows; row++) {
        for (int column = 0; column < columns; column++) {
            Integer thread_local_row = row;
            Integer thread_local_column = column;
            Thread t = new Thread(() -> {
                countNeighbors(arr, thread_local_row, thread_local_column, rows, columns);
                evaluateNeighbors(arr[thread_local_row][thread_local_column], futureGen[thread_local_row][thread_local_column]);
            });
            t.start();
            threads.add(t);
        }
    }
    for(Thread t : threads)
        t.join();
}

Конечным результатом является то, что каждая ячейка получит свой собственный выделенный поток. Обратите внимание, как мало нам пришлось изменить после рефакторинга кода.

Однако, как я уже упоминал, это избыточно с точки зрения количества создаваемых потоков. Таким образом, альтернативой является создание нового потока для каждой строки.

public void checkN(Cell [][] arr, Cell [][] futureGen, int columns, int rows)
{
    ArrayList<Thread> threads = new ArrayList<>();
    for (int row = 0; row < rows; row++) {
        Integer thread_local_row = row;
        Thread t = new Thread(() -> {
            for (int column = 0; column < columns; column++) {
                countNeighbors(arr, thread_local_row, column, rows, columns);
                evaluateNeighbors(arr[thread_local_row][column], futureGen[thread_local_row][column]);
            }
        });
        t.start();
        threads.add(t);
    }
    for(Thread t : threads)
        t.join();
}

Это лучше, но, конечно, это снова может стать излишним, если у вас есть односторонняя сетка с большим количеством строк, но очень небольшим количеством столбцов.

Таким образом, третий вариант — сделать количество потоков постоянным и масштабировать рабочую нагрузку на поток в соответствии с количеством потоков.

public void checkN(Cell [][] arr, Cell [][] futureGen, int columns, int rows)
{
    ArrayList<Thread> threads = new ArrayList<>();
    final int NUM_OF_THREADS = 8; //Or can be passed as an argument
    for(int tid = 0; tid < NUM_OF_THREADS; tid++) {
        Integer thread_local_row_start = tid * rows / NUM_OF_THREADS;
        Integer thread_local_row_end = (tid + 1) * rows / NUM_OF_THREADS;
        Thread t = new Thread(() -> {
            for (int row = thread_local_row_start; row < thread_local_row_end; row++) {
                for (int column = 0; column < columns; column++) {
                    countNeighbors(arr, row, column, rows, columns);
                    evaluateNeighbors(arr[row][column], futureGen[row][column]);
                }
            }
        });
        t.start();
        threads.add(t);
    }
    for(Thread t : threads)
        t.join();
}

Этот вариант имеет тенденцию быть наиболее производительным, поскольку вы можете установить NUM_OF_THREADS равным количеству ядер процессора в вашей машине или любому значению, которое вы найдете в тестировании, чтобы обеспечить идеальную производительность.

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

Если вы ограничены Java 7, весь написанный код по-прежнему можно использовать, но лямбда-структуру необходимо заменить анонимным внутренним классом, а любые переменные, которые необходимо использовать в теле потока, необходимо сделать final :

public void checkN(final Cell [][] arr, final Cell [][] futureGen, final int columns, final int rows)
{
    ArrayList<Thread> threads = new ArrayList<>();
    for (int row = 0; row < rows; row++) {
        for (int column = 0; column < columns; column++) {
            final Integer thread_local_row = row;
            final Integer thread_local_column = column;
            Thread t = new Thread(new Runnable() {
                public void run() {
                    countNeighbors(arr, thread_local_row, thread_local_column, rows, columns);
                    evaluateNeighbors(arr[thread_local_row][thread_local_column], futureGen[thread_local_row][thread_local_column]);
                }
            });
            t.start();
            threads.add(t);
        }
    }
    for(Thread t : threads)
        t.join();
}
person Xirema    schedule 01.12.2017
comment
Большое спасибо за столь подробный ответ! Я так удивлен, насколько короче стала функция checkN! Показывает, как многому я должен научиться. Я знаю, что это действительно бесполезно и я считаю неэффективным создавать поток для каждой ячейки, но это домашнее задание. Мы никогда не создавали темы, и я полагаю, это был способ научиться. Однако я нахожу это довольно запутанным, даже после просмотра большого количества видео о них. Еще раз большое спасибо. Я еще не реализовал это, но, тем не менее, я очень ценю величественный ответ! - person Nano; 02.12.2017