Принуждение нескольких потоков использовать несколько процессоров, когда они доступны - PullRequest
66 голосов
/ 03 августа 2009

Я пишу программу на Java, которая использует много ЦП из-за характера того, что она делает. Однако многие из них могут работать параллельно, и я сделал свою программу многопоточной. Когда я запускаю его, кажется, что он использует только один процессор, пока ему не нужно больше, чем другой процессор - могу ли я что-нибудь сделать в Java, чтобы заставить разные потоки работать на разных ядрах / процессорах?

Ответы [ 10 ]

56 голосов
/ 03 августа 2009

Существует два основных способа многопоточности в Java. Каждая логическая задача, которую вы создаете с помощью этих методов, должна запускаться на новом ядре, когда это необходимо и доступно.

Метод первый: определить объект Runnable или Thread (который может принимать Runnable в конструкторе) и запустить его с помощью метода Thread.start (). Он будет работать на любом ядре, которое дает ОС - как правило, менее загруженном.

Учебное пособие: Определение и запуск потоков

Метод два: определяют объекты, реализующие интерфейс Runnable (если они не возвращают значения) или Callable (если они делают), которые содержат код обработки. Передайте их как задачи в ExecutorService из пакета java.util.concurrent. Класс java.util.concurrent.Executors имеет множество методов для создания стандартных, полезных видов ExecutorServices. Ссылка на учебник для исполнителей.

Исходя из личного опыта, исправленные и кэшированные пулы потоков в Executors очень хороши, хотя вы захотите изменить количество потоков. Runtime.getRuntime (). AvailableProcessors () может использоваться во время выполнения для подсчета доступных ядер. Когда приложение будет готово, вам необходимо будет закрыть пулы потоков, иначе приложение не закроется, поскольку потоки ThreadPool продолжают работать.

Получение хорошей многоядерной производительности иногда сложно и полно ошибок:

  • Дисковый ввод / вывод замедляет LOT при запуске в параллельно. Только один поток должен выполнять чтение / запись диска одновременно.
  • Синхронизация объектов обеспечивает безопасность многопоточных операций, но замедляет работу.
  • Если задачи слишком тривиальный (маленькие рабочие биты, выполнить быстро) накладные расходы на управление ими в ExecutorService стоит больше, чем вы получаете от нескольких ядер.
  • Создание новых объектов Thread выполняется медленно. ExecutorServices попытается повторно использовать существующие потоки, если это возможно.
  • Все виды сумасшедших вещей могут случиться, когда над чем-то работают несколько потоков Сделайте вашу систему простой и постарайтесь сделать задачи логически различными и не взаимодействующими.

Еще одна проблема: контролировать работу сложно! Хорошей практикой является наличие одного потока менеджера, который создает и отправляет задачи, а затем пара рабочих потоков с рабочими очередями (используя ExecutorService).

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


Редактировать - пример с использованием ExecutorService:

public class TaskThreader {
    class DoStuff implements Callable {
       Object in;
       public Object call(){
         in = doStep1(in);
         in = doStep2(in);
         in = doStep3(in); 
         return in;
       }
       public DoStuff(Object input){
          in = input;
       }
    }

    public abstract Object doStep1(Object input);    
    public abstract Object doStep2(Object input);    
    public abstract Object doStep3(Object input);    

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        ArrayList<Callable> tasks = new ArrayList<Callable>();
        for(Object input : inputs){
           tasks.add(new DoStuff(input));
        }
        List<Future> results = exec.invokeAll(tasks);
        exec.shutdown();
        for(Future f : results) {
           write(f.get());
        }
    }
}
29 голосов
/ 04 августа 2009

Когда я запускаю его, кажется, он использует один процессор, пока ему не нужно больше, чем он использует другой процессор - есть ли что-нибудь я можно сделать в Java, чтобы заставить разные нити бегать по разным Ядра / ЦП? * * 1002

Я интерпретирую эту часть вашего вопроса как означающую, что вы уже рассмотрели проблему обеспечения многопоточности вашего приложения. И, несмотря на это, он не сразу начинает использовать несколько ядер.

Ответ "есть ли способ заставить ..." - (AFAIK) не напрямую. Ваша JVM и / или хост-ОС решают, сколько «нативных» потоков использовать, и как эти потоки отображаются на физические процессоры. У вас есть несколько вариантов для настройки. Например, я нашел эту страницу , в которой рассказывается, как настроить потоки Java в Solaris. И эта страница говорит о других вещах, которые могут замедлять многопоточное приложение.

18 голосов
/ 14 сентября 2010

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

Работа на одном и том же ядре имеет много преимуществ. Кэш процессора горячий, это означает, что данные для этой программы загружены в процессор. Объекты блокировки / мониторинга / синхронизации находятся в кеше ЦП, что означает, что другим ЦП не нужно выполнять операции синхронизации кеша по шине (дорого!).

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

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

8 голосов
/ 04 августа 2009

Во-первых, я бы предложил прочитать «Параллелизм на практике» Брайана Гетца .

alt text

Это, безусловно, лучшая книга, описывающая параллельное программирование на Java.

Параллельность - это «легко учиться, трудно учиться». Я бы посоветовал прочитать много об этом предмете, прежде чем пытаться это сделать. Очень легко заставить многопоточную программу работать правильно в 99,9% случаев и с ошибками 0,1%. Однако вот несколько советов, с которых можно начать:

Существует два распространенных способа заставить программу использовать более одного ядра:

  1. Заставить программу работать с несколькими процессами. Примером является Apache, скомпилированный с Pre-Fork MPM, который назначает запросы дочерним процессам. В многопроцессорной программе память не используется по умолчанию. Однако вы можете отобразить разделы общей памяти между процессами. Apache делает это с помощью своего «табло».
  2. Сделать программу многопоточной. В многопоточной программе вся кучная память распределяется по умолчанию. Каждый поток все еще имеет свой собственный стек, но может получить доступ к любой части кучи. Как правило, большинство программ на Java являются многопоточными, а не многопроцессными.

На самом низком уровне можно создавать и уничтожать темы . Java облегчает создание потоков в переносимой кроссплатформенной манере.

Поскольку создание и уничтожение потоков постоянно обходится дорого, Java теперь включает в себя Исполнители для создания пулов потоков многократного использования. Задачи могут быть назначены исполнителям, а результат может быть получен через объект Future.

Как правило, у кого-то есть задача, которую можно разделить на более мелкие задачи, но конечные результаты необходимо свести воедино. Например, с помощью сортировки слиянием можно разделить список на все более мелкие части, пока каждое ядро ​​не выполнит сортировку. Однако, поскольку каждый подсписок отсортирован, его необходимо объединить, чтобы получить окончательный отсортированный список. Поскольку эта проблема «разделяй и властвуй» является довольно распространенной, существует инфраструктура JSR , которая может обрабатывать базовое распределение и объединение. Эта структура, вероятно, будет включена в Java 7.

4 голосов
/ 07 декабря 2009

Нет способа установить привязку к процессору в Java. http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4234402

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

1 голос
/ 12 апреля 2016

Вы можете использовать API ниже от Исполнители с версией Java 8

public static ExecutorService newWorkStealingPool()

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

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

С grepcode , реализация newWorkStealingPool выглядит следующим образом

/**
     * Creates a work-stealing thread pool using all
     * {@link Runtime#availableProcessors available processors}
     * as its target parallelism level.
     * @return the newly created thread pool
     * @see #newWorkStealingPool(int)
     * @since 1.8
     */
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
1 голос
/ 14 сентября 2010

Настройка производительности JVM упоминалась ранее в Почему этот код Java не использует все ядра ЦП? . Обратите внимание, что это относится только к JVM, поэтому ваше приложение уже должно использовать потоки (и более или менее «правильно»):

http://ch.sun.com/sunnews/events/2009/apr/adworkshop/pdf/5-1-Java-Performance.pdf

1 голос
/ 14 сентября 2010

Я думаю, что эта проблема связана с Java Parallel Proccesing Framework (JPPF). С его помощью вы можете запускать разные задания на разных процессорах.

1 голос
/ 03 августа 2009

Вы должны написать свою программу для выполнения своей работы в виде лота Callable, переданного ExecutorService и выполненного с invokeAll (...).

Затем вы можете выбрать подходящую реализацию во время выполнения из класса Executors. Было бы предложено вызвать Executors.newFixedThreadPool () с номером, приблизительно соответствующим количеству ядер процессора, чтобы оставаться занятым.

1 голос
/ 03 августа 2009

Самое простое, что нужно сделать - разбить вашу программу на несколько процессов. ОС распределит их по ядрам.

Несколько сложнее разбить вашу программу на несколько потоков и доверить JVM правильное их распределение. Это - обычно - то, что люди делают, чтобы использовать доступное оборудование.


Редактировать

Каким образом многопроцессорная программа может быть "проще"? Вот шаг в конвейере.

public class SomeStep {
    public static void main( String args[] ) {
        BufferedReader stdin= new BufferedReader( System.in );
        BufferedWriter stdout= new BufferedWriter( System.out );
        String line= stdin.readLine();
        while( line != null ) {
             // process line, writing to stdout
             line = stdin.readLine();
        }
    }
}

Каждый шаг в конвейере имеет аналогичную структуру. 9 строк служебной информации для любой обработки.

Это может быть не самым эффективным. Но это очень просто.


Общая структура ваших параллельных процессов не является проблемой JVM. Это проблема ОС, поэтому используйте оболочку.

java -cp pipline.jar FirstStep | java -cp pipline.jar SomeStep | java -cp pipline.jar LastStep

Осталось только разработать сериализацию для ваших объектов данных в конвейере. Стандартная сериализация работает хорошо. Прочитайте http://java.sun.com/developer/technicalArticles/Programming/serialization/ для подсказок о том, как сериализовать. Для этого вы можете заменить BufferedReader и BufferedWriter на ObjectInputStream и ObjectOutputStream.

...