Как стать автором
Обновить

Размышление об Active Object в контексте Qt6. Часть 2

Время на прочтение4 мин
Количество просмотров2.4K

Ссылки на статьи

Предисловие

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

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии2

Публикации

Истории

Работа

Программист C++
104 вакансии
QT разработчик
3 вакансии

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань