Насколько хороша JVM при параллельной обработке? Когда я должен создавать свои собственные потоки и Runnables? Почему темы могут мешать? - PullRequest
4 голосов
/ 24 апреля 2009

У меня есть Java-программа, которая запускает множество небольших симуляций. Он запускает генетический алгоритм, где каждая фитнес-функция представляет собой симуляцию с использованием параметров каждой хромосомы. Каждый из них занимает около 10 секунд, если запускается сам по себе, и я хочу запустить довольно большую популяцию (скажем, 100?). Я не могу начать следующий раунд симуляций, пока не закончился предыдущий. У меня есть доступ к машине с множеством процессоров, и мне интересно, нужно ли мне что-то делать, чтобы симуляции работали параллельно. Я никогда не писал ничего явно для многоядерных процессоров, и я понимаю, что это сложная задача.

Итак, вот что я хотел бы знать: в какой степени и насколько хорошо JVM работает параллельно? Я читал, что он создает темы низкого уровня, но насколько он умен? Насколько это эффективно? Будет ли моя программа работать быстрее, если я сделаю каждую симуляцию потоком? Я знаю, что это огромная тема, но не могли бы вы указать мне некоторую вводную литературу, касающуюся параллельной обработки и Java?

Большое спасибо!

Обновление: Хорошо, я реализовал ExecutorService и заставил мои небольшие симуляции реализовать Runnable и методы run (). Вместо того чтобы писать это:

Simulator sim = new Simulator(args); 
sim.play(); 
return sim.getResults(); 

Я пишу это в своем конструкторе:

ExecutorService executor = Executors.newFixedThreadPool(32);

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

RunnableSimulator rsim = new RunnableSimulator(args); 
exectuor.exectue(rsim); 
return rsim.getResults(); 

Метод RunnableSimulator::run() вызывает метод Simulator::play(), ни один из них не имеет аргументов.

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

Каждое моделирование - это собственный объект, верно? На основании приведенного выше кода? Я могу передать точно такой же набор аргументов классам RunnableSimulator и Simulator, и работоспособная версия будет выдавать исключения. Как вы думаете, что может вызвать это и что я могу сделать, чтобы предотвратить это? Могу ли я предоставить несколько примеров кода в новом вопросе, чтобы помочь?

Ответы [ 5 ]

11 голосов
/ 24 апреля 2009

Учебник по параллелизму Java

Если вы просто порождаете кучу вещей в разные потоки, и между разными потоками речь не пойдет, это не так уж сложно; просто напишите каждый в Runnable и передайте их в ExecutorService .

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

По сути, вы делаете что-то вроде этого:

ExecutorService executorService = Executors.newFixedThreadPool(n);

где n - это количество вещей, которые вы хотите запустить одновременно (обычно это количество процессоров). Каждая из ваших задач должна быть объектом, который реализует Runnable, и вы затем выполняете его на ExecutorService:

executorService.execute(new SimulationTask(parameters...));

Executors.newFixedThreadPool(n) запустит n потоков и при выполнении вставит задачи в очередь, которая подает эти потоки. Когда задача заканчивается, поток, в котором она выполнялась, больше не занят, и следующая задача в очереди начнет выполняться на ней. Выполнить не будет блокировать; он просто поместит задачу в очередь и перейдет к следующей.

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

РЕДАКТИРОВАТЬ: Читая ваши правки на ваш вопрос, похоже, что вы действительно хотите что-то немного другое. Вместо реализации Runnable реализовать Callable. Ваш call() метод должен быть почти таким же, как ваш текущий run(), за исключением того, что он должен return getResults();. Тогда submit() это к вашему ExecutorService. Вы получите Future взамен, который вы можете использовать, чтобы проверить, выполнено ли моделирование, и, когда это произойдет, получить свои результаты.

4 голосов
/ 24 апреля 2009

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

1 голос
/ 26 марта 2011

Java-потоки слишком тяжелые. Мы реализовали параллельные ветви в Ateji PX как очень легкие запланированные объекты. Как и в Erlang, вы можете создать десятки миллионов параллельных ветвей, прежде чем начнете замечать накладные расходы. Но это все же Java, поэтому вам не нужно переключаться на другой язык.

0 голосов
/ 27 апреля 2009

Java довольно хорош в параллельной обработке, но есть два предостережения:

  • Java-потоки относительно тяжелые (по сравнению, например, с Erlang), поэтому не начинайте создавать их сотнями или тысячами. Каждый поток получает свою собственную стековую память (по умолчанию: 256 КБ), и, помимо прочего, вы можете исчерпать память.
  • Если вы работаете на очень мощной машине (особенно с большим количеством процессоров и большим объемом оперативной памяти), то настройки виртуальной машины по умолчанию (особенно в отношении GC) могут привести к неоптимальной производительности, и вам, возможно, придется потратить несколько раз на настройку их через параметры командной строки . К сожалению, это не простая задача и требует много знаний.
0 голосов
/ 24 апреля 2009

Если вы все время выполняете полную обработку ваших потоков, вы не выиграете, если у вас будет больше потоков, чем процессоров. Если ваши потоки время от времени ждут друг друга или систему, то Java хорошо масштабируется до тысяч потоков.

Я написал приложение, которое обнаружило сеть класса B (65 000) за несколько минут, выполнив эхо-запрос каждого узла, и каждый эхо-запрос имел повторные попытки с возрастающей задержкой. Когда я помещал каждый пинг в отдельный поток (это было до NIO, я мог бы, вероятно, улучшить его сейчас), я мог запустить до 4000 потоков в окнах, прежде чем все стало плохо. Линукс число было ближе к 1000 (Никогда не выяснил, почему).

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

В Java есть другие инструменты для управления единицами самостоятельной работы, для получения дополнительной информации обратитесь к пакету «Параллельный».

...