Разработка примера использования библиотеки Intel® Threading Building Blocks

Статья содержит описание работы по подбору оптимальной архитектуры взаимодействия библиотеки Intel® Threading Building Blocks и библиотеки асинхронного ввода/вывода boost::asio. Рассматриваются возможные проблемы полученных решений, проведено сравнение реализаций при помощи разработанного benchmark-клиента. В качестве примера построено небольшое игровое приложение «MMOLG Точки Online».

Введение

Intel® Threading Building Blocks (TBB) является библиотекой, которая упрощает написание многопоточных приложений, предлагая пользователям C++ дополнительные возможности абстрагирования для развертывания параллельных приложений. В библиотеке TBB разработчики используют привычные шаблоны C++, а сама библиотека отвечает за низкоуровневые детали многопоточности. Кроме того, TBB обеспечивает совместимость с различными архитектурами и операционными системами.

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

Что, зачем и для чего?

Планировщик TBB предполагает, что задачи имеют конечный объем работы, не содержат длительной блокировки внутри и их достаточно много. Поэтому нельзя эффективно реализовать блокирующиеся задачи, в частности использующие ввод/вывод.

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

Для таких задач наиболее оптимально использовать асинхронный ввод/вывод, однако, несмотря на несколько сообщений на форуме Intel о том, что такая архитектура работоспособна, конкретная реализация в публичном доступе отсутствует, и тем более не ясно, насколько эффективно подобное решение.

Для реализации асинхронного ввода/вывода была взята библиотека boost::asio как наиболее распространенная кроссплатформенная разработка.

Архитектура boost::asio

Модель ввода/вывода boost::asio

Рисунок 1. Модель ввода/вывода boost::asio

Основной объект boost::asio - это io_service. Он владеет сокетами, обрабатывает запросы на асинхронные операции и вызывает по их завершению предоставленные пользователем callback-методы. Вызов callback-методов происходит в контексте Dispatch-потока, который запрашивает io_service обработать все или несколько завершенных запросов.

Для регистрации операции ввода/вывода и получения списка завершенных запросов io_service использует API конкретной операционной системы, поэтому детали его реализации могут существенно отличаться. Например, в Windows используется overlapped I/O, а в Linux - epoll.

Сервер и его архитектура

Для тестирования полученных решений интеграции ввода/вывода и TBB был взят самый яркий пример программы с активным вводом/выводом - TCP сервер. С одной стороны, большая нагрузка на модуль ввода/вывода - вполне жизненная ситуация, с другой - активные параллельные вычисления на сервере тоже встречаются достаточно часто, и потому использование TBB также не будет лишним.

Архитектура сервера

Рисунок 2. Архитектура сервера

Для облегчения тестирования сервер содержит взаимозаменяемые модули ввода/вывода (в последней версии проекта содержатся наиболее интересные архитектуры - Thread per client, Set of dispatcher threads и Single dispatcher + TBB Pipeline, более подробно о которых будет рассказано ниже).

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

Синхронная реализация - Thread per client

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

%Временная диаграмма модели Thread per client

Рисунок 3. Временная диаграмма модели Thread per client

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

 

Таким образом, к достоинству реализации можно отнести:

  • Простота реализации на сокетах Беркли без дополнительных библиотек.

К недостаткам:

  • Значительное потребление ресурсов на создание и освобождение потоков при увеличении числа клиентов.
  • Проблема балансировки количества обслуживающих клиентов потоков и рабочих потоков TBB.

Асинхронные реализации

Наиболее интересными асинхронными архитектурами среди полученных оказались:

  • Set of dispatcher threads
  • Single dispatcher + TBB Tasks
  • Single dispatcher + TBB Pipeline

Рассмотрим подробно каждую из них.

Set of dispatcher threads

Эта архитектура является в своем роде классической для boost::asio. При запуске сервера создается множество Dispatch потоков, которые обрабатывают результаты запросов и вызывают в своем контексте callback-методы. Они, в свою очередь, обрабатывают запрос и ставят для TBB задачи на выполнение запроса. Затем, если необходимо, инициируется новая асинхронная операция ввода/вывода.

Временная диаграмма модели Set of dispatcher threads

Рисунок 4. Временная диаграмма модели Set of dispatcher threads

Такая модель обладает своими достоинствами:

  • Простота реализации на boost::asio.
  • Меньшее, чем в синхронной модели, количество потоков и потому лучшее соотношение работа/ожидание.
  • Динамическое распределение нагрузки по обработке результатов между потоками.

И недостатками:

  • Как и в синхронной реализации, существует проблема балансировки Dispatch потоков с рабочими потоками TBB.
  • При обработке результатов несколькими потоками появляются накладные расходы на синхронизацию внутри io_service.
  • Dispatch-потоки, которые ставят задачи, не участвуют в их выполнении, поэтому рабочие потоки TBB вынуждены тратить время на их перераспределение между собой. Обработка запроса на задачах TBB (а не только его выполнение) в таких условиях неоправданна и ложится на Dispatch-потоки.

Single dispatcher + TBB Tasks

Эта модель была построена с учетом недостатков предыдущей архитектуры: вместо множества Dispatch потоков используется один, который ставит TBB задачи на обработку и выполнение, а затем сам участвует в работе.

Временная диаграмма модели Single dispatcher + TBB TasksРисунок 5. Временная диаграмма модели Single dispatcher + TBB Tasks

Видно, что недостатки предыдущей архитектуры переходят в достоинства этой модели:

  • Нет накладных расходов на синхронизацию внутри io_service
  • Dispatch-поток в рамках callback-методов только формирует очередь задач, обработка и выполнение происходит параллельно рабочими потоками TBB, в том числе и Dispatch-потоком.
  • Постановка новых задач производится по окончанию обработки и выполнения - не допускается перегрузка очереди запросов.

Но также возникают свои проблемы:

  • Во время обработки задач сервер не опрашивает io_service, и, наоборот, для формировании следующей очереди задач обработка задач должна быть окончена.

Single dispatcher + TBB Pipeline

В предыдущей архитектуре проявился очевидный конвейерный характер обработки и выполнения запросов, для которого более логично использовать параллельный примитив tbb::pipeline, а не tbb:task, поэтому модель Single dispatcher + TBB Tasks является скорее промежуточным звеном на пути к модели Single dispatcher + TBB Pipeline, чем независимым решением.

Первая стадия конвейера (Dispatcher) - работа Dispatch-потока по вызову callback-методов, которые, так же как и в предыдущей модели, формируют очередь задач, однако передают их не TBB Task, а на вторую стадию конвейера (Worker pipeline filter). Такое решение позволяет Dispatch-стадии продолжать опрашивать io_service, не дожидаясь окончания обработки и выполнения задач.

Для того чтобы обеспечить достаточный параллелизм, и, в то же время, не допустить перегрузку очереди запросов, количество токенов tbb::pipeline было ограничено двумя. Тем самым, как и в предыдущей реализации, составление очереди новых событий приостанавливается, если сформировано достаточно работы.

Временная диаграмма модели Single dispatcher + TBB Pipeline

Рисунок 6. Временная диаграмма модели Single dispatcher + TBB Pipeline

Достоинства такой архитектуры, помимо наследованных от предыдущей модели:

  • Обработка задач может происходить одновременно с формированием новой очереди.

Такая модель не является идеальной, так, первая стадия конвейера последовательна, так как возможность ее параллельного запуска ограничивается особенностями архитектуры boost::asio (одна из них - невозможность из callback-метода «узнать», в рамках какого экземпляра стадии он вызван). Стоит заметить, что существуют способы обойти все эти проблемы, однако получившийся код будет слишком громоздким и сложным, что неприемлемо для примера.

 

Преобладание последовательной первой стадии конвейера хорошо видно на рисунках 7, 8. Здесь ось X - время, Y - потоки, фиолетовые блоки - первая последовательная стадия конвейера, остальные блоки (розовые, красные и зеленые) - различные параллельные части второй стадии.

Результаты тестирования реализации Single Dispatcher + TBB Pipeline

Рисунок 7. Результаты тестирования реализации Single Dispatcher + TBB Pipeline при помощи Jumpshot-4 и прототипа нового коллектора, разрабатываемого Intel, 100 соединений

Результаты тестирования реализации Single Dispatcher + TBB Pipeline

Рисунок 8. Результаты тестирования реализации Single Dispatcher + TBB Pipeline при помощи Jumpshot-4 и прототипа нового коллектора, разрабатываемого Intel, 10 000 соединений

При большой нагрузке в 10 000 соединений хорошо видно, что большая часть времени уходит на обработку первой последовательной стадии, а потому ее распараллеливание наиболее вероятно приведет к росту производительности.

Методы тестирования

В первую очередь нужно сказать о метриках, которые были использованы при сравнении полученных реализаций. В качестве измеряемых параметров были выбраны следующие:

  • Среднее время ответа
    (наиболее актуален для пользователя)
  • Максимально достижимая загрузка
    (характеризует устойчивость к пиковым загрузкам)

Для получения конкретного значения сервер в течение некоторого промежутка времени держался под максимальной нагрузкой, и каждую секунду с него снимались значения измеряемых параметров. После этого бралось среднее арифметическое по полученной выборке, по значению которого уже и производилось сравнение архитектур.

 

Для тестирования полученных реализаций был создан специальный benchmark-клиент, позволяющий как обеспечивать пиковую нагрузку на сервер, так и собирать статистику. У данного клиента, также как и у сервера, есть синхронная и асинхронная реализации. Как показало сравнение, более точные значения времени ответа получались с использованием синхронной версии клиента. С другой сторону, асинхронная версия позволяла посылать больше запросов на сервер, что важно для измерения второго параметра - достижимой пиковой загрузки.

Сравнение производительности реализаций

По итогам работы были широко протестирована производительность модулей ввода/вывода 3 наиболее важных реализаций: Thread per client, Set of dispatcher threads и Dispatcher + TBB Pipeline.

Тестирование производилось на 24 ядерном сервере нижегородского офиса Intel, работающего под управлением операционной системы Linux. В качестве результата бралось среднее арифметическое по выборке за 10 минут.

Результаты тестирования

Рисунок 9. Результаты тестирования

Как и ожидалось, синхронная архитектура серьезно проигрывает асинхронным реализациям, между архитектурами Set of dispatcher threads и Dispatcher + TBB Pipeline разница не так значительна (в сравнении с синхронной версией она практически не видна).

Такой результат говорит о хорошей интеграции TBB и boost::asio в последней реализации, так как производилось тестирование только не имеющего вычислительной нагрузки модуля ввода/вывода. Оптимизация TBB для таких задач не является приоритетной, и потому прогнозировалось снижение производительности, однако произошел даже небольшой рост по всем показателям. Кроме того, архитектура Dispatcher + TBB Pipeline тесно интегрирована с TBB, а потому обеспечивает автоматическую балансировку ввода/вывода и вычислительной работы, что может обеспечить лучшее время ответа и/или большую максимально достижимую нагрузку.

Стоит заметить, при аналогичном тестировании на обычном 2 ядерном компьютере с операционной системой Microsoft Windows XP соотношение результатов сильно отличалось от вышеприведенного, а также достаточно часто наблюдалось временное «бездействие» связки benchmark-client + server. Это объясняется особенностями реализации данной операционной системы, в которой практически любое действие пользователя (такое как, например, переключение окон) сказывается на результатах тестирования, а также работой антивируса.

Игровая составляющая

Результатом решения задачи летней школы должен был стать открытый пример интеграции библиотек Intel TBB и boost::asio. Для более эффективной демонстрации созданных архитектур было решено реализовать простейшую логику игры «Точки». Стоит отметить, что данная игровая составляющая также позволяет создавать вычислительную нагрузку и моделировать более жизненные ситуации, в которых сервер динамически создает html-страницы (а не только отсылает готовые), а также выполняет некоторую логику.

Вся игровая механика была реализована как отдельная статическая библиотеке, которая хранит состояние всех игр и по запросам пользователя создает нужную html-страницу. Для реализации взаимодействия с пользователем были использованы JavaScript вставки, которые проверяли корректность действий и формировали запросы для сервера.

Интерфейс игрового клиента

Рисунок 10. Интерфейс игрового клиента

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

Итоги и дальнейшие пути TBB в асинхронном мире

По итогам проекта были достигнуты основные цели: разработаны архитектуры взаимодействия TBB и ввода/вывода, рассмотрены возможные проблемы полученных решений, проведено сравнение реализаций при помощи разработанного benchmark-клиента, в качестве примера построено небольшое игровое приложение.

Конечно, архитектура Dispatcher + TBB Pipeline не является законченной и существует множество мелких и крупных дальнейших улучшений, которые не были первоочередными, и на реализацию которых не хватило времени в рамках летней школы. Так, возможна статическая балансировка подключений между несколькими io_service, что может увеличить максимально возможную пиковую нагрузку сервера.

Если рассматривать пути дальнейшего развития серии примеров в целом, то наиболее продуктивным выглядит прямое использование интерфейсов ввода/вывода, предоставляемых операционной системой, без использования дополнительных слоев абстракции, накладывающих свои ограничения.

Результат такого развития будет скорее не примером, а уже библиотекой, предоставляющей программисту простые интерфейсы работы с асинхронным вводом/выводом, основанные на TBB и обеспечивающие тесную и прозрачную интеграцию TBB и асинхронного ввода/вывода.

Все разработанное решение, содержащее реализацию рассмотренных архитектур, benchmark-клиент, игровой модуль, а также историю развития можно найти на портале проекта: http://tbbdots.codeplex.com.

Об авторах

Сергей Гризан, студент Сибирского Федерального Университета, Красноярск, проходил стажировку в рамках Летней Школы Intel в Нижнем Новгороде, которая оказалась интереснейшей и полезной заменой традиционного летнего отдыха. В ходе стажировки я получил неоценимый опыт работы по проекту, общения с отличными профессионалами и просто хорошими, интересными людьми, а также узнал множество полезной информации на лекциях и практических занятиях. В рамках проекта занимался разработкой интерфейсов, архитектуры решения в целом, а так же множества реализаций сервера.

Максим Кривов, студент 5-ого курса факультета Вычислительной Математики и Кибернетики МГУ им. М.В. Ломоносова. От прохождения стажировки в Нижнем Новгороде осталась масса впечатлений, которые точно запомнятся на всю жизнь. При решении задачи летней школы отвечал за разработку benchmark-клиента и реализацию игровой составляющей.

Также благодарим Антона Малахова и Mike Voss за предоставленные дополнительные материалы.

For more complete information about compiler optimizations, see our Optimization Notice.