Создание очереди заданий - Повышение производительности работы библиотеки GridMD

Для организации работы потоков был выбран паттерн проектирования Пул потоков (Thread Pool) [16] . Пул потоков является объектом, которому возможна выдача некоего множества задач на исполнение. Внутри пул потоков представляет собой ограниченный набор инициализированных потоков, которым пул раздает задачи на исполнение из очереди задач пула по мере ее заполнения в соответствии с конкретной схемой планирования назначения потоков на задачи. Поток в пуле может находиться в двух состояниях - ожидания получения задачи, когда поток спит, не занимая процессорное время, и состоянии непосредственного исполнения задачи. Задачи пулом по мере получения собираются в очереди задач и могут быть получены в любой момент существования пула. Пул гарантирует, что в определенный момент времени вновь пришедшая задача будет выполнена, когда до нее дойдет очередь.

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

Использование пула потоков является оптимальным решением для распараллеливания выполнения локальных узлов в GridMD. Помимо преимуществ, указанных выше, использование пула в качестве отдельного модульного элемента позволяет инкапсулировать функционал по организации работы с задачами, генерируемыми менеджером сценариев, таким как ожидание завершения выполнения задачи, опрос статуса исполнения и получение результата задачи, в рамки одного компонента. Такой подход в реализации библиотеки виден в выделении отдельных компонентов, как менеджер сценариев и менеджер заданий. Для координирования исполнения задач в рамках потоков необходимо реализовывать отдельный менеджер, хорошим кандидатом на место которого выступает пул потоков (Рис. 8).

пул потоков в контексте компонентов gridmd

Рис. 8 Пул потоков в контексте компонентов GridMD

Взаимодействие с пулом происходит через его интерфейсные функции и состоит из отправки задачи на исполнение в очередь пула, опроса статуса исполнения задачи, ожидания завершения выполнения задачи с получением ее результата и отмены исполнения в случае необходимости. Логически, пул потоков является Вычислительным ресурсом для локальных задач, генерируемых менеджером сценариев. С точки зрения реализации пул не оформлен в виде ресурса, как это сделано с командным интерпретатором локальной операционной системы или удаленной системой очередей и нет необходимости указывать его в списке ресурсов в файле Resources. xml. Вместо этого, пользователь в секции настройки численного эксперимента при необходимости указывает, что локальные узлы могут быть выполнены с помощью пула, передавая значение перечислимого типа GmEXE_THREADS в метод GmExperiment::set_execution().

Пул потоков реализован в GridMD в виде класса GmThreadPool и в своей реализации имеет два основных компонента - класс рабочего потока GmThread и класс исполняемой задачи GmTask. При создании пулу потоков передается необходимое число рабочих потоков, которое по умолчанию равно числу ядер процессора, умноженному на два.

Рассмотрим более подробно интерфейс класса GmThreadPool:

    - Конструктор GmThreadPool (size_t threads=wxThread::GetCPUCount()-1) Инициализирует Threads рабочих потоков на исполнение в режим ожидания поступления новых задач в очередь задач. Параметром по умолчанию является оптимальное число потоков для данной аппаратной конфигурации, минус один, поскольку работа самого пула потоков выполняется в отдельном потоке, необходимость чего будет рассмотрена ниже. - ~gmThreadPool (). Деструктор пула потоков. Вызывается при выходе объекта пула потоков из области видимости, или явного удаления оператором Delete В случае выделения пула в динамической памяти. Немедленно останавливает все рабочие потоки, прерывая выполнение их текущих задач, освобождает связанные с потоками системные ресурсы и удаляет все оставшиеся задачи из очереди задач. - GmTaskID CreateGMMainTask (int (*gridmd_main)(int, char*[]), int argc, char* argv[]). Метод отправки пулу задачи по исполнению функции Int ((*gridmd_main)(int, char*[]), int argc). Пул потоков работает с абстракциям задач реализованных в виде класса GmTask и его наследников. Метод создает задачу в виде объекта класса GmMainTask, Наследника класса GmTask, Кладет задачу в очередь задач и устанавливает ей статус "принято на исполнение". Возвращает уникальный идентификатор принятой задачи GmTaskID, под которым задача регистрируется в пуле в реестре идентификаторов, и который возможно передать в другие интерфейсные функции пула для работы с конкретной задачей. - GmTASK_STATUS TaskStatus (gmTaskID taskID) const. Метод возвращает статус отправленной пулу задачи в виде перечислимого типа GmTASK_STATUS по уникальному идентификатору задачи TaskID. Поддерживаются следующие состояния задачи:
      ? GmTASK_POOLED - Задача принята на исполнение, в текущий момент ожидает исполнения в очереди задач. ? GmTASK_PROCESSED - Задача исполняется рабочим потоком. ? GmTASK_FINISED - задача успешно завершена, возможно получение ее возвращаемого значения с помощью метода Int TaskResult (gmTaskID taskID). ? gmINVALID_STATUS - Задача с идентификатором TaskID не зарегистрирована в пуле. Такое возможно, если идентификатор TaskID был получен не с помощью одного из методов пула по созданию задач, или задача была явно удалена из пула после вызова других методов, рассмотренных ниже.
    - Int TaskResult (gmTaskID taskID). Метод получения возвращаемого значения задачи. При вызове ожидает завершение исполнения задачи, а именно до момента, когда ее статус станет равен GmTASK_FINISHED. Ожидание завершения задачи происходит в синхронном режиме, блокируя вызывающий метод поток. Возвращает число, сигнализирующее об успешности выполнения задачи, в соответствии с логикой, заложенной пользователем (например, при выполнении Int (*gridmd_main)(int, char*[], int argc) ее возвращаемое значение). Удаляет задачу из пула потоков вместе с ее идентификатором из реестра идентификаторов, делая идентификатор TaskID невалидным для остальных интерфейсных функций. - Void RemoveTask (gmTaskID taskID). Метод удаления задачи с идентификатором TaskID. В зависимости от статуса задачи удаляет ее из очереди задач или немедленно останавливает ее исполнение, удаляя связанный с ней объект типа GmTask и ее идентификатор из реестра идентификаторов задач пула, делая идентификатор TaskID невалидным для остальных интерфейсных функций. - Bool IsValidIndex(gmTaskID taskID) const. Метод проверки, зарегистрирована ли задача с идентификатором TaskID в реестре идентификаторов. - Void RegisterRedirector(gmRedirectorBase* redirector). В пуле потоков для получения уникальной копии конкретного объекта для каждой из исполняемых задач была реализована концепция Редиректоров. Редиректор представляется в виде шаблонного класса GmRedirector, Который параметризируется типом хранимого объекта и хранит уникальную копию объекта для каждой задачи. Копию объекта можно получить с помощью метода gmRedirector::getobject(). Для использования редиректора его необходимо зарегистрировать с помощью рассматриваемого метода пула, передав указатель на базовой класс редиректора GmRedirectorBase*. Использование базового класса позволяет хранить уникальные объекты разного типа для каждой из задач.

Рассмотрим отдельные компоненты и некоторые детали реализации пула потоков.

Класс GmTask Является абстракцией задачи, выполняемой пулом потоков. gMTask Является абстрактным классом, от которого наследуются конкретные классы задач, реализуя чисто виртуальный метод GmTask::Run(). Метод Run() Позволяет определить действия, связанные с выполнением задачи, и исполняется рабочим потоком пула, когда до задачи доходит очередь. Таким образом, возможно определение различных типов задач и абстрагирование от их типа при исполнении в пуле. Типизация задач связанна с различными способами определения действий, связываемых с узлами графа исполнения.

диаграмма классов реализации пула потоков

Рис. 9 Диаграмма классов реализации пула потоков

В данный момент реализован класс GmMainTask для исполнения задачи, генерируемой менеджером сценариев, при Неявном определении действий, а именно для исполнения функции Gridmd_main(). В будущем планируется реализовать поддержку исполнения задач в пуле при явном определении действий с помощью класса GmNodeAction и при определении действий в виде скриптов.

диаграмма состояний объекта класса gmtask

Рис. 10 Диаграмма состояний объекта класса GmTask

На Рис. 10 представлена диаграмма состояний класса GmMainTask. Жизненный цикл задачи, исполняемой пулом, начинается с создания объекта GmTask и отправки задачи в очередь заданий пула с помощью метода GmThreadPool::GreateGMMainTask(). Задача переходит в состояние GmTASK_POOLED, При котором она ожидает своей очереди на исполнение. Когда задача становится в начале очереди, ее забирает первый освободившийся поток и вызовом метода GmThread::StartTask() начинает исполнение задачи, вызывая метод GmMainTask::Run() и устанавливая ее в состояние GmTASK_PROCESSED. После завершения метода GmMainTask::Run() исполнение задачи завершено, поток устанавливает состояние задачи в GmTASK_FINISHED и забирает новую задачу в случае, если очередь задач не пуста. В состоянии GmTASK_FINISED вызов метода GmThreadPool::TaskResult() Немедленно возвращает число, сигнализирующее об успешности выполнения задачи, в соответствии с логикой, заложенной пользователем, и удаляет объект GmMainTask Вместе с ее уникальным идентификатором из реестра идентификаторов пула. Пул не хранит информацию о выполненной и опрошенной о результатах задаче, и метод опроса статуса GmThreadPool::TaskStatus() по такому идентификатору вернет GmINVALID_STATUS.

В состояниях GmTASK_POOLED, gmTASK_PROCESSED, gmTASK_FINISHED Возможно явно отменить выполнение задачи и уничтожить связанный с ней объект методом GmThreadPool::RemoveTask(), который установит состояние задачи в GmINVALID_STATUS. Реализация механизма удаления состоит либо в удалении задачи из очереди в случае, если она находится в статусе GmTASK_POOLED, Или удалении связанного с ней потока при нахождении задачи в состоянии GmTASK_PROCESSED. Не существует другого способа отцепить задачу от рабочего потока пула, кроме явного удаления потока с освобождением связанных с ним ресурсов и его переинициализации. Но для восстановления исходного количества рабочих потоков после удаления задачи в классе пула потоков необходим механизм, который будет производить мониторинг текущего состояния потоков пула, получать сигнал об удалении задачи и инициализировать новый поток вместо удаленного. Именно поэтому класс GmThreadPool, как и его рабочие потоки GmThread, является наследником класса WxThread И переопределяет метод WxThread::Entry(), Который работает в отдельном потоке, ожидая сообщения об удалении рабочего потока и инициализируя новый вместо удаленного.

В приведена реализация восстановления количества рабочих потоков при удалении одного из них. Метод GmThreadPool:: Entry() Ожидает сигнала об удалении потока с помощью условной переменной MThreadsNotifier И ее метода Wait(). В деструкторе GmThread Происходит вызов MThreadNotifier. Signal(), Который будит ожидающий служебный поток пула, и проверяет условие равентва текущего количества потоков переданному пользователем при создании пула. Если они не равны, то служебный поток блокирует связанный с массивом потоков мьютекс, чтобы предотвравить конкуретный доступ к ней (при удалении еще одной задачи деструктор другого потока обратиться к масссиву для удаления себя из него), инициализирует необходимое количество потоков и добавляет их в массив.

При отсутствии в очереди задач каждый из рабочих потоков GmThread в переопределенном методе GmThread::Entry() ожидает сигнала о прибытии в очередь новой задачи с помощью условной переменной MQueueNotifier, входящей в состав объекта GmThreadPool, и ее метода Wait(). В методе GmThreadPool::SubmitTask(gmTask *task) Задача кладется в очередь, о чем сигнализируется одному из ожидающих потоков методом MQueueNotifier. Wait(). Кто именно из ожидающих потоков получит сообщение о получении новой задачи не определено и зависит от планировщика операционной системы, и в данной реализации это не имеет важности. После получения сообщения поток просыпается, блокирует связанный с очередью задач мьютекс MQueueMutex для получения эксклюзивного доступа к очереди задач и начинает выполнение задачи вызовом метода GmThread::StartTask(). После завершения выполнения задачи поток пытается взять новую задачу из очереди. В этом случае, если очередь не пуста, распределение задач по потокам определяется случайным образом и зависит от последовательности получения каждым из них исключительного доступа к критической секции путем блокировки мьютекса MQueueMutex. Поток, занявший критическую секцию, получает первую задачу из очереди.

Важной особенностью представленной реализации пула потоков является его модульность и высокая степень сокрытия реализации. Пул потоков самостоятельно управляет ресурсами, такими как рабочие потоки и задачи, выделяя их по внешнему запросу и освобождая по мере использования. Внешние компоненты, взаимодействующие с пулом (например, менеджер сценариев), не имеют доступа к объектам рабочих потоков GmThread и задач GmTask, им лишь выдаются идентификаторы для работы с ресурсами через интерфейс пула. Поддерживаемый набор типов задач так же является расширяемым через наследование от класса GmTask, интерфейс которого не зависит от других компонентов библиотеки GridMD. Таким образом, реализованный пул потоков имеет слабую связность с остальными компонентами библиотеки, не зависит от их реализации и может быть использован обособленно в других проектах.

Похожие статьи




Создание очереди заданий - Повышение производительности работы библиотеки GridMD

Предыдущая | Следующая