Каковы общие причины высокой загрузки ЦП?
Фон:
В моем приложении, написанном на С++, я создал 3 потока:
- AnalysisThread (или Продюсер): он считывает входной файл, анализирует его и генерирует шаблоны и помещает их в очередь
std::queue
1.
- PatternIdRequestThread (или Потребитель): он удаляет шаблоны из очереди и отправляет их один за другим в базу данных через клиент (написанный на С++), который возвращает шаблон uid, который затем присваивается соответствующему шаблону.
- ResultPersistenceThread: он делает несколько вещей, разговаривает с базой данных, и он отлично работает, как и ожидалось, в отношении использования ЦП.
Первые два потока занимают 60-80% от использования ЦП, каждый из которых занимает в среднем 35%.
Вопрос:
Я не понимаю, почему некоторые потоки используют высокую загрузку процессора.
Я анализирую его следующим образом: если именно OS принимает решения, такие как контекстный переключатель, interrupt и scheduling в отношении того, какой поток должен получить доступ к системным ресурсам, например как процессорное время, то почему некоторые потоки в процессе используют больше процессора, чем другие? Похоже, что некоторые потоки принудительно перехватывают процессор из ОС под дулом пистолета, или у ОС есть реальное мягкое пятно для некоторых потоков, и поэтому оно смещено к ним с самого начала, предоставляя им все ресурсы, которые у него есть. Почему он не может быть беспристрастным и не дает им равных?
Я знаю, что это наивно. Но я смущаюсь больше, если я думаю по этой строке: ОС дает доступ к процессору к потоку, исходя из объема работы, которую должен выполнять поток, но как ОС вычисляет или прогнозирует объем работы до ее выполнения полностью?
Интересно, в чем причины высокой загрузки процессора? Как мы можем их идентифицировать? Можно ли их идентифицировать, просто взглянув на код? Каковы инструменты?
Я использую Visual Studio 2010.
1. Я тоже сомневаюсь в std::queue
. Я знаю, что стандартные контейнеры не являются потокобезопасными. Но если ровно один поток помещает объекты в очередь, то безопасно ли, если точно один элемент потока потока из него? Я полагаю, что это похоже на трубку, с одной стороны вы вставляете данные, с другой - удаляете данные, тогда почему это было бы небезопасно, если бы это было сделано одновременно? Но это не настоящий вопрос в этой теме, однако вы можете добавить примечание в свой ответ, обращаясь к этому.
Обновление:
После того, как я понял, что мой потребительский поток использует занятый спин, который я исправил с помощью Sleep в течение 3 секунд. Это исправление является временным, и вскоре я буду использовать Событие. Но даже с Sleep, использование ЦП снизилось до 30-40%, а иногда оно достигает 50%, что, похоже, не желательно с точки зрения удобства использования, поскольку система не отвечает на другие приложения, с которыми пользователь в настоящее время работает.
Есть ли способ улучшить производительность процессора? Как было сказано ранее, поток производителя (который в настоящее время использует большинство циклов процессора) читает файл, анализирует в нем пакеты (некоторого формата) и генерирует из них шаблоны. Если я использую сон, то использование ЦП уменьшится, но будет ли это хорошей идеей? Каковы общие способы его решения?
Ответы
Ответ 1
Лично я был бы очень раздражен, если бы мои потоки работали, и на моей машине были свободные ядра, потому что ОС не давала им высокой загрузки процессора. Поэтому я действительно не вижу здесь проблемы. [Edit: turn out your busy looping - проблема, но в принципе нет ничего плохого в использовании процессора).
OS/scheduler в значительной степени не прогнозирует объем работы, которую будет выполнять поток. Поток (для чрезмерного упрощения) в одном из трех состояний:
- заблокировано в ожидании чего-то (сон, мьютекс, ввод-вывод и т.д.)
- runnable, но в настоящее время не выполняется, потому что другие вещи
- работает.
Планировщик выберет как можно больше вещей, поскольку у него есть ядра (или гиперпотоки, что угодно), и запускайте каждый из них либо до тех пор, пока он не блокируется, либо пока не истечет произвольный период времени, называемый "таймлисом". Тогда он заплатит что-то еще, если это возможно.
Итак, если поток затрачивает большую часть своего времени на вычисления, а не на блокирование, и если там нет ядра, то он будет занимать много процессорного времени.
Там много деталей в том, как планировщик выбирает, что запускать, на основе таких вещей, как приоритет. Но основная идея заключается в том, что поток, который нужно сделать много, не обязательно должен быть предсказан как вычислительный, он всегда будет доступен, когда что-то нужно планировать, и, следовательно, будет иметь тенденцию планироваться.
В вашем примере цикла ваш код фактически ничего не делает, поэтому вам нужно будет проверить, как он был оптимизирован, прежде чем судить о том, имеет ли смысл 5-7% CPU. В идеале, на двухъядерной машине обрабатывающий поток должен занимать 50% процессор. На 4-х основных машинах - 25%. Поэтому, если у вас нет по меньшей мере 16 ядер, тогда ваш результат на первый взгляд аномальный (и если у вас было 16 ядер, то одна нить, занимающая 35%, была бы еще более аномальной!). В стандартной настольной ОС большинство ядер неактивны большую часть времени, поэтому чем выше доля процессора, который ваши фактические программы занимают, когда они работают, тем лучше.
На моей машине я часто сталкиваюсь с одним основным достоинством использования ЦП при запуске кода, который в основном обрабатывает текст.
если ровно один элемент очереди очереди в очередь, то это безопасно, если точно один элемент потока потока из него?
Нет, это небезопасно для std::queue
со стандартным контейнером. std::queue
представляет собой тонкую упаковку поверх контейнера последовательности (vector
, deque
или list
), она не добавляет никакой безопасности потоков. Нить, добавляющая элементы и поток, удаляющий элементы, изменяет некоторые общие данные, например поле size
в базовом контейнере. Вам нужна либо некоторая синхронизация, либо безопасная блокировка структуры очереди, которая опирается на атомный доступ к общим данным. std::queue
не имеет.
Ответ 2
Изменить: Хорошо, поскольку вы используете занятие для блокировки в очереди, это, скорее всего, является причиной высокой загрузки ЦП. У ОС создается впечатление, что ваши потоки выполняют полезную работу, когда они на самом деле нет, поэтому они получают полное процессорное время. Здесь была интересная дискуссия: Какой из них лучше для производительности, чтобы проверять другие потоки в java.
Я советую вам либо переключиться на события или другие блокирующие механизмы, либо использовать некоторую синхронизированную очередь и посмотреть, как это происходит.
Кроме того, это рассуждение о том, что очередь является потокобезопасной "потому что только два потока используют ее", очень опасна.
Предполагая, что очередь реализована как связанный список, представьте, что может произойти, если она содержит только один или два элемента. Поскольку у вас нет способа контролировать относительные скорости производителя и потребителя, это вполне может быть так, и поэтому у вас большие проблемы.
Ответ 3
Прежде чем вы начнете думать о том, как оптимизировать потоки, чтобы потреблять меньше CPU, вам нужно иметь представление о том, где все это время процессора. Один из способов получить эту информацию - использовать профилировщик ЦП. Если у вас его нет, дайте Very Sleepy попробовать. Он прост в использовании и свободен.
Профайлер процессора будет отслеживать ваше запущенное приложение и делать заметки о том, где время тратится. В результате он предоставит вам список функций, отсортированных по тому, сколько CPU они использовали в течение периода выборки, сколько раз было вызвано и т.д. Теперь вам нужно посмотреть результаты профилирования, начиная с большинства интенсивных функций процессора и посмотрите, что вы можете изменить в тех, которые уменьшают использование ЦП.
Важно то, что как только у вас есть результаты профилирования, у вас есть фактические данные, которые расскажут вам, какие части вашего приложения вы можете оптимизировать, чтобы получить наибольший доход.
Теперь рассмотрим виды вещей, которые вы можете найти, которые потребляют много CPU.
-
Рабочий поток обычно реализуется как цикл. В верхней части цикла выполняется проверка, чтобы решить, есть ли работа, и выполняется любая доступная работа. Новая итерация цикла снова начинает цикл.
Вы можете обнаружить, что с такой настройкой большая часть времени процессора, выделенного для этого потока, тратится на цикл и проверку, и очень мало потрачено на собственно работу. Это так называемая проблема "ожидание". Чтобы частично решить эту проблему, вы можете добавить sleep
между циклами, но это не лучшее решение. Идеальный способ решить эту проблему - поставить поток в режим сна, когда нет работы, и когда какой-то другой поток генерирует работу для спящего потока, он посылает сигнал, чтобы разбудить его. Это практически исключает накладные расходы, поток будет использовать процессор только тогда, когда есть работа. Обычно я реализую этот механизм с помощью семафоров, но в Windows вы также можете использовать объект Event. Вот эскиз реализации:
class MyThread {
private:
void thread_function() {
while (!exit()) {
if (there_is_work_to_do())
do_work();
go_to_sleep();
}
}
// this is called by the thread function when it
// doesn't have any more work to do
void go_to_sleep() {
sem.wait();
}
public:
// this is called by other threads after they add work to
// the thread queue
void wake_up() {
sem.signal();
}
};
Обратите внимание, что в приведенном выше решении функция потока всегда пытается заснуть после выполнения одной задачи. Если очередь потоков имеет больше рабочих элементов, то ожидание на семафоре будет немедленно возвращено, так как каждый раз, когда элемент был добавлен в очередь, отправитель должен был вызвать функцию wake_up().
-
Другая вещь, которую вы можете увидеть на выходе профилировщика, заключается в том, что большая часть процессора расходуется на функции, выполняемые рабочим потоком, когда он выполняет работу. Это на самом деле не плохо, если большую часть времени тратится на работу, то это означает, что поток работал, и было доступно время процессора для выполнения этой работы, поэтому в принципе здесь нет ничего плохого.
Но, тем не менее, вы не можете быть счастливы, что ваше приложение использует так много CPU, поэтому вам нужно взглянуть на способы оптимизации вашего кода, чтобы он делал работу более эффективно.
Например, вы можете обнаружить, что некоторая небольшая вспомогательная функция была вызвана миллионы раз, поэтому, хотя один запуск функции выполняется быстро, если вы умножаете это на несколько миллионов, это становится шеей бутылки для потока. На этом этапе вы должны посмотреть, как сделать оптимизацию для уменьшения использования ЦП в этой функции, либо путем оптимизации его кода, либо путем оптимизации вызывающего абонента (-ов), чтобы вызывать функцию меньше времени.
Итак, стратегия здесь состоит в том, чтобы начать с самой дорогой функции в соответствии с профилирующим отчетом и попытаться сделать небольшую оптимизацию. Затем вы запустите профайлер, чтобы посмотреть, как все изменилось. Вы можете обнаружить, что небольшое изменение в самой интенсивной работе процессора переводит его на 2-е или 3-е место, и в результате общее использование ЦП было уменьшено. После того как вы поздравляете себя за улучшение, вы повторяете упражнение с новой верхней функцией. Вы можете продолжить этот процесс, пока не убедитесь, что ваше приложение так же эффективно, как и может быть.
Удачи.
Ответ 4
Хотя другие правильно проанализировали проблему уже (насколько я могу судить), позвольте мне попытаться добавить более подробно к предлагаемым решениям.
Во-первых, чтобы обобщить проблемы:
1. Если вы держите потребительский поток занятым, вращающимся в петле или подобном, это ужасная трата мощности процессора.
2. Если вы используете функцию sleep() с фиксированным количеством миллисекунд, это тоже трата процессора (если слишком мало времени), или вы затягиваете процесс без необходимости (если он слишком высок). Невозможно установить правильное время.
Вместо этого вам нужно использовать тип сна, который просыпается только в нужный момент, то есть всякий раз, когда к очереди добавляется новая задача.
Я объясню, как это сделать с помощью POSIX. Я понимаю, что не идеален, когда вы находитесь в Windows, но, чтобы извлечь из этого выгоду, вы можете использовать библиотеки POSIX для Windows или использовать соответствующие функции, доступные в вашей среде.
Шаг 1: Вам нужен один мьютекс и один сигнал:
#include <pthread.h>
pthread_mutex_t *mutex = new pthread_mutex_t;
pthread_cond_t *signal = new pthread_cond_t;
/* Initialize the mutex and the signal as below.
Both functions return an error code. If that
is not zero, you need to react to it. I will
skip the details of this. */
pthread_mutex_init(mutex,0);
pthread_cond_init(signal,0);
Шаг 2: Теперь внутри потребительского потока дождитесь отправки сигнала. Идея заключается в том, что производитель посылает сигнал всякий раз, когда добавляет новую задачу в очередь:
/* Lock the mutex. Again, this might return an error code. */
pthread_mutex_lock(mutex);
/* Wait for the signal. This unlocks the mutex and then 'immediately'
falls asleep. So this is what replaces the busy spinning, or the
fixed-time sleep. */
pthread_cond_wait(signal,mutex);
/* The program will reach this point only when a signal has been sent.
In that case the above waiting function will have locked the mutex
right away. We need to unlock it, so another thread (consumer or
producer alike) can access the signal if needed. */
pthread_mutex_unlock(mutex);
/* Next, pick a task from the queue and deal with it. */
Шаг 2 выше должен по существу быть помещен в бесконечный цикл. Убедитесь, что существует способ выхода из цикла. Например, хотя и немного грубо - вы можете добавить "специальную" задачу в очередь, которая означает "выйти из цикла".
Шаг 3: Включить поток производителя для отправки сигнала всякий раз, когда он добавляет задачу в очередь:
/* We assume we are now in the producer thread and have just appended
a task to the queue. */
/* First we lock the mutex. This must be THE SAME mutex object as used
in the consumer thread. */
pthread_mutex_lock(mutex);
/* Then send the signal. The argument must also refer to THE SAME
signal object as is used by the consumer. */
pthread_cond_signal(signal);
/* Unlock the mutex so other threads (producers or consumers alike) can
make use of the signal. */
pthread_mutex_unlock(mutex);
Шаг 4: Когда все будет закончено, и вы отключите свои потоки, вы должны уничтожить мьютекс и сигнал:
pthread_mutex_destroy(mutex);
pthread_cond_destroy(signal);
delete mutex;
delete signal;
Наконец, позвольте мне повторить повторение одной вещи, о которой уже говорили другие: вы не должны использовать обычный std::deque
для одновременного доступа. Один из способов решения этого - объявить еще один мьютекс, заблокировать его перед каждым доступом к deque и сразу разблокировать его.
Изменить: Еще несколько слов о потоке производителя, в свете комментариев. Насколько я понимаю, поток производителей в настоящее время может добавлять столько задач в очередь, сколько может. Поэтому я предполагаю, что он будет продолжать делать это и поддерживать работу процессора в той степени, в которой он не задерживается IO и доступом к памяти. Во-первых, я не думаю о высоком использовании ЦП в результате этой проблемы, а скорее в качестве выгоды. Тем не менее, одна серьезная проблема заключается в том, что очередь будет расти бесконечно, что может привести к тому, что процесс исчерпает пространство памяти. Следовательно, полезно принять меры предосторожности, чтобы ограничить размер очереди до разумного максимума и приостановить поток производителей всякий раз, когда очередь увеличивается слишком долго.
Чтобы реализовать это, поток производителя проверяет длину очереди перед добавлением нового элемента. Если он заполнен, он поставит себя в спящий режим, ожидая, когда сигнал будет отправлен потребителем при выполнении задания из очереди. Для этого вы можете использовать механизм вторичного сигнала, аналогичный описанному выше.
Ответ 5
Нити потребляют ресурсы, такие как память. Блокирующая/разблокирующая нить несет однократную стоимость. Если поток блокирует/разблокирует десятки тысяч раз в секунду, это может привести к потере значительных объемов процессора.
Однако, как только поток блокируется, не имеет значения, сколько времени он заблокирован, нет текущих затрат.
Популярным способом поиска проблем с производительностью является использование профилировщиков.
Однако, я делаю это много, и мой метод таков: http://www.wikihow.com/Optimize-Your-Program%27s-Performance
Ответ 6
Использование процессора потоков зависит от многих факторов, но в основном ОС может назначать время обработки только на основе точек, в которых он может прервать поток.
Если ваш поток взаимодействует с оборудованием в любом случае, это дает ОС возможность прервать поток и назначить обработку в другом месте, в основном на основе предположения, что аппаратное взаимодействие требует времени. В вашем примере вы используете библиотеку iostream и, таким образом, взаимодействуете с оборудованием.
Если в вашем цикле не было этого, то, скорее всего, он будет использовать почти 100% процессор.
Ответ 7
- использовать асинхронный (файл и сокет) IO для уменьшения времени ожидания процессора.
- используйте вертикальную модель потоковой передачи, чтобы уменьшить контекстный переключатель, если возможно
- использовать структуру данных без блокировки
- используйте инструмент профилирования, такой как VTune, чтобы разобраться в горячей точке и сделать оптимизацию
Ответ 8
Как говорили люди, правильным способом синхронизации передачи между потоком производителя и потребителя будет использование переменной условия. Когда производитель хочет добавить элемент в очередь, он блокирует переменную условия, добавляет элемент и уведомляет официантов в переменной условия. Потребитель ждет одну и ту же переменную условия, и когда она уведомляется, она потребляет элементы из очереди, а затем снова блокируется. Я лично рекомендовал бы использовать boost:: interprocess для них, но это можно сделать достаточно простым способом, используя другие API.
Кроме того, следует иметь в виду, что хотя концептуально каждый поток работает только на одном конце очереди, большинство библиотек реализуют метод O (1) count()
, что означает, что у них есть переменная-член для отслеживания количество элементов, и это возможность для редких и трудно диагностируемых проблем concurrency.
Если вы ищете способ уменьшить использование процессора потребительского потока (да, я знаю, что это ваш реальный вопрос)... ну, похоже, он фактически делает то, что он должен сейчас, но обработка данных стоит дорого. Если вы сможете проанализировать, что он делает, могут быть возможности для оптимизации.
Если вы хотите разумно дросселировать поток производителя... это немного больше работает, но вы можете заставить поток производителей добавлять элементы в очередь до тех пор, пока не достигнет определенного порога (скажем, 10 элементов), а затем подождите другой переменная условия. Когда потребитель потребляет достаточное количество данных, это приводит к тому, что количество элементов в очереди окажется ниже порогового значения (например, 5 элементов), после чего оно уведомляет эту вторую переменную условия. Если все части системы могут быстро перемещать данные, тогда это может по-прежнему потреблять много CPU, но оно будет распространяться относительно равномерно между ними. В этот момент ОС должна нести ответственность за то, чтобы другие независимые процессы получили свою справедливую (иш) долю в процессоре.