Целью настоящей лабораторной работы является изучение способов снижения накладных расходов на поддержку многопоточности. Под накладными расходами понимаются непроизводительные издержки на работу с потоками (время их создания, управления и уничтожения), а также время, затрачиваемое на работу с примитивами синхронизации (например, время с момента
В настоящей лабораторной работе используется учебное приложение ClientServer, которое имитирует работу системы, имеющей клиент-серверную архитектуру. Особенность состоит в том, что для простоты клиент и сервер не представляют собой различные процессы, а реализованы в виде потоков одного процесса. Сценарий приложения следующий: клиент посылает запросы нескольких типов, а сервер принимает и обрабатывает их. Обработка производится в специально создаваемых для этого потоках.
Откройте проект ClientServer, последовательно выполняя следующие шаги:
C:\ITPLab\ClientServer,ClientServer.sln или, выбрав файл, выполните команду Open.После открытия проекта в окне Solution Explorer выберите проект ClientServer и дважды щелкните на файле исходного кода ClientServer., как это показано на рис. 14.1. После этих действий программный код, с которым предстоит работать, будет открыт в рабочей области Microsoft Visual Studio.
(рис 14.1) Открытие файла ClientServer.cppВ начале файла ClientServer. объявлены две константы:
const int numRequests = 100; const int numTypes = 4;
Первая из них указывает количество запросов от клиента к серверу, вторая показывает, сколько существует различных типов запросов. В данном случае клиент передаст серверу 100 запросов четырех типов. Структура запроса в нашем приложении предельно проста - это целое число от 0 до numTypes. То есть клиент каждый раз посылает серверу число, которое и означает тип пришедшего запроса.
Кроме того, в приложении как глобальные переменные объявлены очередь и вектор:
queue<int> requests; //queue for requests vector<int> requestsStatistics; //requests statistics
Очередь используетcя для передачи запросов от клиента к серверу, а вектор для сохранения статистики о пришедших запросах. Так, например, значение requestsStatistics[0] показывает количество пришедших запросов типа 0.
Ознакомьтесь с кодом приложения. Как можно видеть, для обработки каждого запроса сервер создает новый поток. Таким образом, приложение состоит из трех компонент:
main - рабочая функция потока-клиента;ServerThreadFunc - рабочая функция потока-сервера;HandlerPoolThreadFunc - рабочая функция потока-обработчика.Скомпилируйте и запустите приложение стандартными средствами Microsoft Visual Studio:
Убедитесь в правильности работы приложения по выводу на консоль.
Запустите процесс
(рис 14.3) Результаты профилирования приложенияОбратите внимание на область розового цвета в окне Timeline. При увеличении видим, что она образована стрелками, соответствующими вызовам . Это тревожный симптом, свидетельствующий о том, что в нашем приложении создается слишком много потоков. Ситуация эта плоха тем, что полезная работа, которую совершают потоки, не компенсирует затраты на их создание/уничтожение.
Действительно, для каждого запроса сервер создает новый поток, поэтому в нашем приложении существует numRequests+2 потока (сервер и клиент). Наш сервер, создавая и уничтожая потоки, тратит больше времени и потребляет больше системных ресурсов, чем если бы обрабатывал запросы самостоятельно. Кроме того, активные потоки также потребляют системные ресурсы, что может привести к нехватке оперативной памяти и значительному падению производительности.
Вообще говоря, работа многих серверов (web-серверы, серверы базы данных) связана с обработкой большого количества коротких запросов от какого-либо удаленного источника (клиента). При этом существует несколько распространенных вариантов архитектуры многопоточного сервера:
Рассмотрим более подробно эти варианты.
Это решение подходит для тех случаев, когда количество запросов к серверу достаточно мало, и обращения к серверу происходят редко. При этом архитектура приложения очень проста, единственной трудностью является построение очереди входящих запросов, необходимой для предотвращения потери запросов при последовательной обработке.
Однако, этот подход крайне неэффективен при высокой частоте обращений к серверу. Если сервер производит обработку самостоятельно, время отклика будет очень большим (пропорционально сложности обработки). В итоге сервер будет не успевать обрабатывать все запросы. Кроме того, если сервер работает на двухъядерном узле, то мы будем наблюдать полную загрузку одного ядра и простой второго.
Именно поэтому в реальных приложениях этот подход используется крайне редко. Мы далее не будем касаться его.
При такой схеме для каждого клиентского запроса создается отдельный поток. В рассматриваемом нами приложении реализован именно этот подход. Сервер имеет следующую архитектуру: основной поток приложения ожидает поступления запросов от клиентов, и при поступлении нового создает поток, передавая клиентский запрос ему на обработку. Созданный поток выполняет соответствующую обработку и завершает свое существование.
Однако и этот подход имеет существенные недостатки:
Пул потоков предлагает решение перечисленных выше проблем. Стандартная схема организации приложения с пулом потоков выглядит следующим образом. Имеется основной поток приложения, ожидающий поступления клиентских запросов, и потоки, составляющие пул, которые создаются заранее или при поступлении первого запроса. При поступлении запроса главный поток выбирает свободный поток из пула и передает запрос ему на обработку, если же свободных потоков нет, то запрос помещается в очередь и ждет освобождения одного из потоков в пуле.
Положительный момент состоит в том, что потоки, однажды созданные, используются многократно, в результате чего издержки на создание и уничтожение потока малы по сравнению с его полезной работой. В итоге сокращается время обработки одного запроса, поскольку поток уже существует, когда прибывает очередной запрос.
Обычно имеет смысл ограничивать общее число рабочих потоков либо числом доступных процессоров (ядер), либо кратным ему (чтобы обеспечить равномерную загрузку ядер). Если потоки занимаются только вычислениями, их число обычно выбирается равным числу ядер. Если же потоки некоторое время находятся в состоянии ожидания (выполнение операций ввода-вывода), то число потоков может превышать число ядер. Обычно ограничивают число потоков удвоенным числом процессоров (ядер).
Посмотрим, какие изменения произойдут в работе нашего приложения при введении пула потоков. Для этого в Microsoft Visual Studio в окне Solution Explorer выберите проект ClientServerPool и дважды щелкните на файле ClientServerPool..
В начале файла ClientServerPool. объявлена новая константа, указывающая количество потоков в пуле:
const int numPoolThreads = 4;
Кроме того, вводится еще одна очередь, используемая сервером для хранения запросов при отсутствии свободных потоков в пуле:
queue<int> serverRequests; //request queue on server
Ознакомьтесь с кодом приложения. После этого запустите процесс
(рис 14.4) Результаты профилирования приложенияПри анализе можно заметить, что непроизводительные издержки значительно снизились, однако все еще присутствуют издержки, связанные с синхронизацией. Рассмотрим их подробнее в следующем разделе.
Основные издержки при синхронизации связаны с доступом потоков к глобальному массиву requestsStatistics, в котором хранится статистика запросов. При этом доступ происходит каждый раз в рамках критической области:
//enter to critical section (lock requestsStatisticsMutex mutex) WaitForSingleObject(requestsStatisticsMutex,INFINITE); //increment counter requestsStatistics[serverRequest]++; //release critical section (unlock requestsStatisticsMutex mutex) ReleaseMutex(requestsStatisticsMutex);
Это сильно увеличивает время работы потоков-обработчиков, поскольку они вынуждены ожидать друг друга. Можно предложить следующие пути решения этой проблемы:
Рассмотрим второй из этих подходов. Закомментируйте критическую область для доступа к requestsStatistics и раскомментируйте следующую строку в рабочей функции потока обработчика:
InterlockedIncrement(reinterpret_cast<long*>(requestsStatistics[serverRequest]));
Проанализируйте произошедшие изменения и время работы приложения с помощью
Можно заметить, что в нашем приложении синхронизация происходит слишком часто (большое количество стрелок желтого цвета). Это вызвано тем, что потоки работают с одними и теми же глобальными объектами, и вынуждены ждать друг друга. Эта проблема довольно часто встречается в многопоточных приложениях, и естественный подход к ее решению - снижение частоты синхронизации.
В нашем случае в качестве решения можно предложить следующий подход:
numTypes очередей, в каждой из которых хранится запросы одинакового типа;requestsStatistics.ClientServerPool?Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.