Для ускорения обработки программы разобьем задачу на несколько задач, которые выполняются параллельно. И создайте пул потоков, чтобы делегировать задачи потокам в пуле потоков, чтобы они могли выполняться одновременно. В случае высокого уровня параллелизма использование пулов потоков может эффективно сократить время и ресурсы, связанные с созданием и выпуском потоков. Если пулы потоков не используются, система может создавать большое количество потоков и потреблять системную память и «чрезмерное переключение». (в механизме обработки, принятом в JVM, используется ротация временных срезов, что уменьшает взаимные переключения между потоками).
Но перед нами стоит большая проблема, то есть мы хотим создать как можно больше задач, но не можем создать слишком много потоков из-за ограниченности ресурсов. Итак, в случае высокой параллелизма, как выбрать оптимальное количество потоков? Каков принцип выбора?
1. Теоретический анализ
Есть две теории о том, как рассчитать количество одновременных потоков.
Первый, «Параллелизм в Java на практике», - это «Практика параллельного программирования на Java», раздел 8.2, стр. 170.
Для задач с интенсивными вычислениями система с процессорами Ncpu обычно достигает оптимального использования за счет использования пула потоков из Ncpu + 1 поток (поток с интенсивными вычислениями случается с ошибкой страницы или приостановлен по другим причинам, просто имея «дополнительный» поток что в этом случае гарантирует, что циклы ЦП не прерывают работу).
Для задач, связанных с вводом-выводом и другими блокирующими операциями, не все потоки будут планироваться постоянно, поэтому вам потребуется больший пул. Чтобы правильно установить длину пула потоков, вы должны оценить отношение времени, которое задачи тратят на ожидание, к времени, затраченному на вычисления; эта оценка не обязательно должна быть точной и может быть получена с помощью некоторых инструментов мониторинга. Вы также можете выбрать другой подход к определению размера пула потоков, запустив приложение с несколькими пулами потоков разных размеров при эталонной нагрузке и наблюдая за уровнем загрузки ЦП.
Учитывая следующие определения:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)
Вы можете использовать Runtime, чтобы получить количество процессоров:
int N_CPUS = Runtime.getRuntime().availableProcessors();
Конечно, циклы процессора — не единственный ресурс, которым можно управлять с помощью пула потоков. Другие ресурсы, которые могут ограничивать размер пула ресурсов, включают: память, дескрипторы файлов, дескрипторы сокетов и соединения с базой данных. Вычислить ограничения по размеру для этих типов пулов ресурсов очень просто: сначала сложите общее количество этих ресурсов, необходимых для каждой задачи, а затем разделите на общее доступное количество. Полученный результат является верхней границей размера пула.
Когда задача требует объединения ресурсов, таких как соединения баз данных, длина длины пула резьбы и ресурсов пула влияет на друг друга. Если каждая задача требует подключения к базе данных, размер пула соединения ограничивает эффективный размер пула резьбы; аналогично, когда потоки задания в пуле является единственным пулом, когда потребитель подключен, затем размер пула резьбы, но он ограничивает Эффективный размер соединительного пула.
Как и выше, в книге «Java Concurrency in Practice» приводится формула для оценки размера пула потоков:
Nthreads = Ncpu x Ucpu x (1 + W/C),其中
Ncpu = CPU核心数
Ucpu = CPU使用率,0~1
W/C = 等待时间与计算时间的比率
Второй, «Параллельное программирование при освоении JVM», - это «Параллельное программирование виртуальной машины Java», раздел 2.1, стр. 12.
Чтобы решить вышеуказанные проблемы, мы хотим создать как минимум столько потоков, сколько ядер процессора. Это гарантирует, что для решения задач будет задействовано как можно больше процессорных ядер. С помощью следующего кода мы можем легко получить количество процессорных ядер, доступных в системе:
Runtime.getRuntime().availableProcessors();
Следовательно, минимальное количество потоков для приложения должно быть равно количеству доступных ядер процессора. Если все задачи требуют больших вычислительных ресурсов, достаточно создать столько потоков, сколько ядер процессора доступно. В этом случае создание большего количества потоков вредно для производительности программы. Потому что при наличии нескольких задач в состоянии готовности ядру процессора необходимо часто выполнять переключение контекста между потоками, а такое переключение приводит к большой потере производительности программы. Но если задачи требуют интенсивного ввода-вывода, нам нужно открыть больше потоков для повышения производительности.
Когда задача выполняет операции ввода-вывода, ее поток будет заблокирован, поэтому процессор может немедленно переключить контекст для обработки других готовых потоков. Если у нас будет столько потоков, сколько доступно ядер процессора, даже ожидающие задачи не могут быть обработаны, потому что у нас больше нет потоков, доступных процессору для планирования.
Если задача блокируется в 50% случаев, программе требуется в два раза больше потоков, чем доступно ядер процессора. Если задачи заблокированы менее чем на 50% времени, т.е. эти задачи являются ресурсоемкими, то количество требуемых программой потоков будет уменьшено, но как минимум не ниже количества ядер процессора. Если задача блокируется дольше времени выполнения, то есть задача является IO-интенсивной, нам необходимо создать количество потоков, в несколько раз превышающее количество ядер процессора. Мы можем рассчитать общее количество потоков, необходимых программе, которое можно резюмировать следующим образом:
- Количество потоков = ЦП можно использовать / (1 - коэффициент блокировки), где коэффициенты блокировки находятся в диапазоне от 0 до 1.
- Коэффициент блокировки для задач с интенсивным вычислением равен 0, а коэффициент блокировки для задач с интенсивным вводом-выводом близок к 1. Полностью блокирующая задача обречена на провал, поэтому нам не нужно беспокоиться о том, что коэффициент блокировки достигнет 1.
Чтобы лучше определить количество потоков, необходимых программе, нам нужно знать следующие два ключевых параметра:
- Количество ядер, доступных процессору;
- Коэффициент блокировки задачи;
Первый параметр легко определить, и мы даже можем найти это значение во время выполнения, используя предыдущий метод. А вот определить коэффициент блокировки несколько сложнее. Сначала мы можем попытаться угадать или использовать некоторые инструменты профилирования или API java.lang.management, чтобы определить соотношение времени, которое потоки тратят на системные операции ввода-вывода, по сравнению со временем, затрачиваемым на задачи, интенсивно использующие ЦП. Как и выше, в книге «Programming Concurrency on the JVM Mastering» приведена формула для оценки размера пула потоков:
Количество потоков = Ncpu / (1 - коэффициент блокировки)
Для утверждения 1, предполагая, что ЦП работает на 100%, то есть исключая коэффициент использования ЦП, количество потоков = Ncpu x (1 + W/C).
Теперь предположим, что формула метода 2 равна формуле метода 1, а именно Ncpu / (1 - коэффициент блокировки) = Ncpu x (1 + W/C), выводим: коэффициент блокировки = W / (W + C), а именно коэффициент блокировки = время блокировки / (время блокировки + время расчета), этот вывод подтверждается в ходе выполнения метода 2 следующим образом:
Так как запросы к веб-сервисам большую часть времени тратят на ожидание ответа сервера, коэффициент блокировки будет довольно высоким, поэтому количество потоков, которое необходимо открыть программе, может в несколько раз превышать количество ядер процессора. Если предположить, что коэффициент блокировки равен 0,9, то есть каждая задача блокируется 90% времени и работает только 10% времени, то на двухъядерном процессоре нам нужно открыть 20 потоков (рассчитывается по формуле в разделе 2.1). Если нужно обработать много акций, мы можем запустить до 80 потоков на 8-ядерном процессоре для обработки задачи.
Видно, что утверждение 1 и утверждение 2 на самом деле являются одной и той же формулой.
2. Практическое применение
Итак, как установить количество одновременных потоков в реальном использовании? Сначала рассмотрим тему:
Предположим, что TPS (транзакций в секунду или задач в секунду) системы составляет не менее 20, а затем предположим, что каждая транзакция выполняется одним потоком, и продолжайте предполагать, что среднее время обработки транзакции каждым потоком составляет 4 с. . Тогда проблема превращается в:
Как спроектировать размер пула потоков, чтобы 20 транзакций могли быть обработаны в течение 1 с?
Процесс расчета очень прост, производительность каждого потока составляет 0,25 транзакций в секунду, тогда для достижения 20 транзакций в секунду, очевидно, необходимо 20/0,25 = 80 потоков.
Теоретически это верно, но на практике самой быстрой частью системы является ЦП, поэтому именно ЦП определяет верхний предел пропускной способности системы. Повышенная вычислительная мощность ЦП может увеличить верхний предел пропускной способности системы. При рассмотрении этого вопроса необходимо учитывать пропускную способность процессора.
Анализ выглядит следующим образом (в качестве примера возьмем формулу 1):
Nthreads = Ncpu x (1 + W/C)
То есть, чем выше доля времени ожидания потока, тем больше потоков требуется. Чем выше процент процессорного времени потока, тем меньше требуется потоков. Эту задачу можно разделить на два типа:
Интенсивный ввод-выводВ целом, если есть IO, то утверждают, что W / C> 1 (время блокировки обычно рассчитывается для расчета времени потребления времени), но необходимо учитывать системную память системной памяти (требуется пространство памяти ), Здесь вам нужно, сколько потоков подходят для тестирования на сервере (пропорция процессора, количество потоков, общее потребление, расход памяти). Если вы не хотите тестировать, сохраните не принимать 1, Nthreads = NCPU X (1 + 1) = 2NCPU. Обычно этот параметр подходит.
вычислительно интенсивныйПредполагая отсутствие ожидания W = 0, тогда W/C = 0. Nпотоки = Ncpus.
В соответствии с эффектом короткой доски реальная пропускная способность системы не может быть рассчитана только на основе ЦП. Чтобы улучшить пропускную способность системы, вам нужно начать с «недостатков системы» (таких как сетевая задержка, ввод-вывод):
- Попробуйте улучшить коэффициент распараллеливания операций с короткими досками, таких как технология многопоточной загрузки;
- Расширение возможностей короткой платы, например замена IO на NIO;
Первое можно связать с законом Амдала, который определяет формулу расчета коэффициента ускорения после распараллеливания последовательной системы: коэффициент ускорения = потребление системного времени до оптимизации / потребление системного времени после оптимизации Чем выше коэффициент ускорения, тем быстрее распараллеливание системы, тем лучше оптимизация. Закон Аддаля также определяет взаимосвязь между параллелизмом системы, количеством ЦП и коэффициентом ускорения.Коэффициент ускорения равен Ускорению, коэффициент сериализации системы (относится к коэффициенту последовательного исполняемого кода) равен F, а количество ЦП равно N. : Ускорение
Когда N достаточно велико, чем меньше коэффициент сериализации F, тем больше коэффициент ускорения Speedup.
В это время возникает вопрос о том, должен ли пул потоков быть более эффективным, чем поток?
Ответ — нет, например, Redis однопоточен, но очень эффективен, а базовые операции могут достигать 100 000 порядков в секунду. С точки зрения многопоточности частично причина в следующем:
- Многопоточность приводит к накладным расходам на переключение контекста потока, а в одиночном потоке таких накладных расходов нет;
- Замок;
Конечно, более существенная причина того, что Redis работает быстро, заключается в том, что:
Redis — это, по сути, операция с памятью, и в этом случае один поток может очень эффективно использовать ЦП. Применимый сценарий многопоточности, как правило, таков: существует значительная доля операций ввода-вывода и сетевых операций.
В целом, ситуация на применении отличается, а многопоточная / одна резьбовая стратегия принимается; если пул резьбы, разные оценки, назначение и исходные точки согласуются.
Пока вывод такой:
IO-intensive = 2Ncpu (вы можете контролировать размер после тестирования, 2Ncpu вообще не проблема) (часто появляется в тредах: взаимодействие с данными базы данных, загрузка и выгрузка файлов, передача данных по сети и т. д.)
Интенсивность вычислений = Ncpu (часто в потоках: сложные алгоритмы)
Конечно, есть и другой способ сказать:
Для задач с интенсивными вычислениями система с процессорами Ncpu обычно достигает оптимального использования за счет использования пула потоков из Ncpu + 1 поток (поток с интенсивными вычислениями случается с ошибкой страницы или приостановлен по другим причинам, просто имея «дополнительный» поток что в этом случае гарантирует, что циклы ЦП не прерывают работу).
То есть вычислительно интенсивный = Ncpu + 1, но здесь не рассматривается целесообразность еще одного переключения контекста ЦП, вызванного этим подходом. Читатели могут решить сами.