Ссылки на статьи
Предисловие
Пришло время написать вторую часть статьи. На этот раз мы рассмотрим нечто, к чему вы скорее всего придёте, работая над многопоточным кодом с использованием 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.