Многопоточный сервер Qt. Пул потоков. Паттерн Decorator

В предыдущей статье [1] была рассмотрена работа с сокетами в библиотеке Qt. Наш сервер сетевого чата работал в одном потоке. Задача текущей статьи — описание многопоточного сервера.

Однако, если сервер просто принимает сообщение и передает его всем подключенным клиентам — распараллеливать нечего и потоки не особо нужны. В связи с этим, мы немного усложним задачу — наш сервер будет вычислять передаваемые арифметические выражения (для вычисления выражений используем Qt Script [2]).

Серверу (а точнее, сокету) надо добавить новый функционал, и в этом нам поможет шаблон проектирования «Декоратор». Когда сервер начнет выполнять все необходимые нам функции, приступим к распараллеливанию. В статье рассмотрены 2 варианта:

  • создание отдельного потока на каждое подключение;
  • использование стандартного пула потоков библиотеки Qt.

В предыдущих статьях уже был описан шаблон параллельного программирования «поставщик-потребитель» [3]. Пул потоков является родственным паттерном, но мы не будем писать свою реализацию, а используем готовую.

Шаблон проектирования Decorator

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

рис. 1 Диаграмма классов шаблона Декоратор

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

Решить проблему можно наследованием, породив подклассы с новой функциональностью от каждого компонента, однако, это не будет гибким решением. Шаблон проектирования декоратор позволяет:

  • добавлять новую функциональность уже существующим объектам прямо во время выполнения. Гамма описывает пример с полосами прокрутки, которые могут появляться лишь при определенных условиях[4];
  • смешивать функциональность. Например, графический элемент может одновременно изменять цвет, иметь привязку к сетке и предоставлять полосу прокрутки — если бы мы решали эту (повседневную и простую, в общем-то) задачу лишь наследованием — иерархия классов получилась бы громадной («комбинаторный взрыв»).
SHEIN Many GEO's

Как показано на рисунке 1, и компонент, и декоратор реализуют один и тот же интерфейс, т.е. декоратор является таким же компонентом, как и все остальные. Тем не менее, Гамма указывает, что в некоторых случаях, декоратор может сильно отличаться от других компонентов и это надо учитываь [4]. Класс Decorator хранит ссылку на декорируемый компонент, а его наследники каким-либо образом переадресуют этому компоненту запросы, но кроме этого, выполняют еще какие-либо действия (рисуют рамку или выводят ползунки прокрутки, например).

Шаблон проектирования «Декоратор». Пример

В нашей задаче многопоточного сервера, декорироваться будет сокет. За основу взят код однопоточного сервера чата [1].

рис. 2 до имплементации шаблона Decorator

На рисунке 2 видно, что сервер работает с указателем на ISocketAdapter, по которому находится экземпляр ServerSocketAdapter, который умеет принимать и отправлять текстовые сообщения. Нам требуется научить сервер новым трюкам, при этом, мы не хотим сильно изменять уже написанный и отлаженный код. Очевидно, надо породить нового наследника ISocketAdapter, который будет делать тоже, что и ServerSocketAdapter и еще чуть-чуть.

рис. 3 decorator pattern implementation

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

Видно, что базовый класс декоратора остается абстрактным, наследует интерфейс сокета и, одновременно, агрегирует сокет и соединяет свои слоты с его сигналами.

CalcSockDecorator по-особенному обрабатывает поступающие сообщения (слот on_message), все остальные к запросы к себе он транслирует агрегируемому объекту. При поступлении сообщения, он пытается выполнить его как выражение ECMAScript, в случае возникновения исключительной ситуации возвращает строку с ошибкой, иначе — результат вычисления. Именно слот on_message выполняет самую сложную работу сервере, которую мы будем пытаться выполнять в отдельном потоке в остальной части статьи.

Коренным образом изменилось только создания сокета — создается экземпляр CalcSockDecorator вместо ServerSocketAdapter. Кроме того, на листинг 3 виден подводный камень — с использованием механизмов библиотеки Qt нам придется следить за тем, чтобы клиентский код был связан с сигналами и слотами декораторов самого верхнего уровня. Иными словами, если пользователь решит передавать сообщения зашифрованными — сервер должен будет:

  1. создать соответствующий декоратор сокета, передав старый сокет в качестве аргумента;
  2. отсоединить весь клиентский код от старого сокета;
  3. соединить клиентский код с декорированным сокетом.

Все это может быть очень трудно реализовываться и, кажется, возможны случаи, в которых из за сигналов и слотов использовать паттерн декоратор будет почти не возможно. Это надо учитывать при проектировании.

Исходный код примера сервера с декорированным сокетом можно скачать.

Многопоточный сервер. Поток на каждого клиента

Очевидно, если мы хотим работать с каждым клиентом в отдельном потоке, то поток должен создаваться в момент подключения. Изменения коснутся лишь метода сервера on_newConnection.

В строках 7-9 описано создание потока и перемещение в него объекта-обработчика сокета. Ничего нового в этом нет и, очевидно, это не лучшее решение. Поток Qt — это поток операционной системы, который, в свою очередь, является достаточно крупным объектом. Если к нашему чату подключится тысяча пользователей, серверу будет очень не легко, даже если пользователи почти не будут общаться. В связи с этим, количество потоков на сервере стараются ограничить.

Можно, например, завести на сервере определенное количество потоков (пул потоков) и направлять новых клиентов к наименее загруженным потокам. Примерно такой вариант мы уже использовали при написании парсера проектов с биржи фриланса [3], но в библиотеке Qt есть готовый пул потоков, который используется чуть-чуть иначе (ниже описано подробнее).

Важно отметить, что в некоторых языках (Erlang, например) поддерживаются легковесные потоки. Для этих языков решение с отдельным потоком на каждого клиента было бы не только самым простым, но правильным и естественным.

Исходный код многопоточного сервера тоже можно скачать.

Пул потоков Qt

Пул потоков Qt (QThreadPool) управляет набором потоков (QThread). Количество потоков задается методом maxThreadCount(), по умолчанию их ровно столько, сколько ядер имеется у вашего процессора. Когда мы писали свой пул, потоки там существовали вечно, но в QThreadPool поток удаляется если в течении определенного времени в него не поступают задачи, задать такой таймаут можно методом setExpiryTimeout(). При поступлении задачи поток вновь будет создан.

Мы могли бы создать на сервере экземпляр QThreadPool и добавлять задачи в него, однако, каждая программа, написанная с использованием Qt уже имеет запущенный пул потоков (глобальный пул программы). Обратиться к глобальному пулу можно с помощью статической функции QThreadPool::globalInstance(). С этим пулом мы и будем работать.

Задача, добавляемая в пул должна наследовать класс QRunnable — это абстрактный класс с чисто виртуальным методом run(). Метод run() должен содержать код, который мы хотим вынести в отдельный поток. После завершения работы, объект может быть автоматически удален, если установлен соответствующий флажок (по умолчанию установлен). Установить или снять такой флажок можно методом QRunnable::setAutoDelete().

Чтобы передать результаты выполнения задачи во внешний мир можно использовать механизм сигналов и слотов, но QRunnable не является наследником QObject, поэтому наша задача для пула будет использовать множественное наследование. Передавать задаче данные, которые должны быть обработаны, удобно во время конструирования объекта.

В QThreadPool мы будем добавлять задачи, связанные с вычислением выражения, полученного через сокет.

Теперь при получении сообщения, объект CalcSockDecorator создает объект Calc и добавляет его в пул вызовом QThreadPool::start(), после чего, ожидает сигнала с результатами от этого объекта.

Вот таким не хитрым образом, почти без изменения уже написанного кода можно распараллелить программу с использованием пула потоков библиотеки Qt.

Скачать исходный код такого замечательного сервера тоже можно.

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

Источник:http://pro-prof.com/archives/1390