Ссылки на статьи
Предисловие
Пришло время написать вторую часть статьи. На этот раз мы рассмотрим нечто, к чему вы скорее всего придёте, работая над многопоточным кодом с использованием Qt.
Снова настоятельно рекомендую ознакомиться с этой статьёй. Она даёт отличный пласт понимания работы Qt и необходима для примеров из этой статьи.
В чём идея
Если вспомнить пример из первой части цикла, то можно сказать, что между client и active object почти всегда стоит очередь (на практике я не встречал active object без очередей). Но если прочитать эту статью, то мы узнаём, что внутри каждого цикла событий Qt лежит очередь.
Таким образом, если обернуть задачу в наследника класса QEvent, то можно без проблем использовать эту очередь для передачи задач. Так ещё и получим возможность задавать приоритет передачи сообщений.
Из коробки в Qt существует Qt::HighEventPriority (1), Qt::NormalEventPriority (0) и Qt::LowEventPriority (-1), но по факту вы можете передавать любое числовое значение в пределах int32_t.
Реализация Event-ориентированного Active object
Для начала создадим класс события. Он будет приватным внутренним классом для нашего Active object. Т.е. никто извне не сможет увидеть тип этого события.
PrinterMessageEvent
class PrinterMessageEvent : public QEvent { private: QPromise<void> m_promise; const QString m_message; public: inline static constexpr QEvent::Type Type = static_cast<QEvent::Type>(QEvent::Type::User + 1); PrinterMessageEvent(const QString &message); const QString& message() const; QPromise<void>& promise(); }; EventBasedAsyncQDebugPrinter::PrinterMessageEvent::PrinterMessageEvent(const QString &message) :QEvent{ Type }, m_message{ message } {} const QString &EventBasedAsyncQDebugPrinter::PrinterMessageEvent::message() const { return m_message; } QPromise<void> &EventBasedAsyncQDebugPrinter::PrinterMessageEvent::promise() { return m_promise; }
Класс события предельно прост. Мы пользуемся тем, что QEvent является move-only классом, а значит, может передавать внутри себя QPromise по значению.
Далее весь класс QEvent сводится к простому DTO с двумя геттерами (для сообщения и промиса).
В конструкторе необходимо указать QEvent::Type больший или равный, чем QEvent::Type::User. Это необходимо, чтобы Qt автоматически передал это событие в виртуальный метод customEvent().
Теперь создадим сам класс Активного объекта. У него будет такой же метод print, возвращающий future, как и в примере из первой части.
EventBasedAsyncQDebugPrinter
class EventBasedAsyncQDebugPrinter : public QObject { private: Q_OBJECT class PrinterMessageEvent : public QEvent { /*...*/ }; public: explicit EventBasedAsyncQDebugPrinter(QObject *parent = nullptr); QFuture<void> print(const QString& message) ; protected: virtual void customEvent(QEvent *event) override; }; EventBasedAsyncQDebugPrinter::EventBasedAsyncQDebugPrinter(QObject *parent) :QObject{ parent } {} QFuture<void> EventBasedAsyncQDebugPrinter::print(const QString &message) { auto task = new PrinterMessageEvent{ message }; auto future = task->promise().future(); qApp->postEvent(this, task); return future; } void EventBasedAsyncQDebugPrinter::customEvent(QEvent *event) { //C++17-if if(auto message = dynamic_cast<PrinterMessageEvent*>(event); message) { qDebug() << message->message(); message->promise().finish(); } }
Имеется такой же метод print, возвращающий future, как и в примере из первой части. В этом методе active object сам себе высылает событие.
Это нужно, потому что метод print может быть вызван из любого потока, а вот метод customEvent, обрабатывающий событие, будет обязательно вызван в потоке, которому принадлежит этот QObject.
Тут нужно отметить тот факт, что метод sendEvent использует QCoreApplication::notify, который не является потокобезопасным. Поэтому, если вы не уверены, что отправитель и получатель события находятся в одном и том же потоке (а здесь мы уверены в обратном), то используйте метод postEvent.
Использование такого класса очень похоже на использование класса из примера прошлой части:
Применение Active object
qDebug() << "Start application"; auto printer = new EventBasedAsyncQDebugPrinter{}; auto printerThread = new QThread{ qApp }; printer->moveToThread(printerThread); printerThread->start(); printer->print("Hello, world!").then(QtFuture::Launch::Async, [printer] { qDebug() << "In continuation"; printer->print("Previous message was printed"); });
Здесь создаётся printer (наш Active object), который затем перемещается moveToThread() в отдельный поток, созданный специально для него. После чего поток запускается (можно переместить и после запуска, эффект не изменится).
Стоит понимать, что данный Active object асинхронен, в отличие от предыдущего. Это значит, что для него вообще необязательно использовать отдельный поток. Но гораздо чаще вы будете использовать это свойство для другой цели: создать отдельный поток, который будет обрабатывать одновременно несколько Active object, выполняющих какие-либо высоконагруженные задачи, которые иначе просто блокировали бы основной поток.
Плюсы
-
Достаточно устройчивая реализация, которая зачастую при минимальной доработке покроет вообще все ваши потребности. Это ли не счастье.
-
Максимальное использование встроенных механизмов Qt.
-
Чтобы удалить все события PrinterMessageEvent из очереди событий, достаточно дёрнуть QCoreApplication::removePostedEvents(printer, PrinterMessageEvent::Type);
Минусы
-
Любой гений тактики может повесить event filter на этот Active object, и перекрыть все события. Но это нестрашно, поскольку так можно поломать вообще весь Qt.
-
Нет встроенного механизма для прекращения приёма входящих сообщений. Можно вставить std::atomic_flag, который решит все ваши проблемы. Причём, это можно как раз сделать через event-filter (что иногда позволяет красиво блокировать сообщения для active objects, не меняя их самих). Но он не то чтобы нужен.
-
Необходимо тщательно документировать все классы событий, создаваемые в вашем коде. Причина этому — QEvent::Type, который должен быть различен у всех классов событий. Как минимум из-за метода QCoreApplication::removePostedEvents, который ориентируется на этот тип (хотя и использовать вы его будете чуть реже, чем никогда).
Заключение
Эта версия Active object сполна может быть использована в реальном коде. В дальнейшем мы рассмотрим более реалистичные примеры использования паттерна, которые могут быть применены в коде «как есть».
Исходный код есть на GitHub.
ссылка на оригинал статьи https://habr.com/ru/post/710368/
Добавить комментарий