Подтвердить что ты не робот

Принуждение нескольких потоков к использованию нескольких процессоров, когда они доступны

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

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

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

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

  • Дисковый ввод-вывод замедляет 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());
        }
    }
}

Ответ 3

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

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

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

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

Ответ 4

Во-первых, я бы предложил прочитать "Concurrency на практике" Брайана Гетца.

alt text

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

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

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

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

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

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

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

Ответ 5

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

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

Ответ 6

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

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


Edit

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

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, чтобы выполнить это.

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

public static ExecutorService newWorkStealingPool()

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

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

Из 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);
    }