Ссылки на статьи
Предисловие
В предыдущей статье я описал способ вызова слота посредством очереди обработки сигнально-слотовых соединений Qt (она же очередь событий). Но совсем забыл про такую штуку, как QMetaObject::invokeMethod. А ведь эта штука позволяет добиться такого же эффекта (вызов метода в потоке-владельце QObject), но без необходимости создания сигнала.
Реализация
Самое простое, что можно сделать
Самый простой способ исправить мою оплошность — удалить из класса сигнал, и заменить его излучение вызовом QMetaObject::invokeMethod.
Заголовок
class MetaInvokeBasedAsyncQDebugPrinter : public QObject { private: Q_OBJECT class PrinterMessage { /*...*/ }; private: std::queue<PrinterMessage> m_messages; std::mutex m_mutex; public: explicit MetaInvokeBasedAsyncQDebugPrinter(QObject *parent = nullptr); QFuture<void> print(const QString& message); private slots: void handleNextMessage(); private: void emplaceTaskInQueue(PrinterMessage&& task); std::queue<PrinterMessage> takeTaskQueue(); };
Как видим, заголовочный файл почти не изменился. Только исчез сигнал, свидетельствовавший о поступлении данных в очередь.
Реализация
MetaInvokeBasedAsyncQDebugPrinter::MetaInvokeBasedAsyncQDebugPrinter(QObject *parent) :QObject{ parent } { } QFuture<void> MetaInvokeBasedAsyncQDebugPrinter::print(const QString &message) { auto task = PrinterMessage{ message }; auto future = task.promise().future(); emplaceTaskInQueue(std::move(task)); QMetaObject::invokeMethod(this, &MetaInvokeBasedAsyncQDebugPrinter::handleNextMessage, Qt::ConnectionType::QueuedConnection); return future; } void MetaInvokeBasedAsyncQDebugPrinter::handleNextMessage() { auto buffer{ takeTaskQueue() }; while(not buffer.empty()) { qDebug() << buffer.front().message(); buffer.front().promise().finish(); buffer.pop(); } } void MetaInvokeBasedAsyncQDebugPrinter::emplaceTaskInQueue(PrinterMessage &&task) { std::lock_guard locker{ m_mutex }; m_messages.emplace(std::move(task)); } std::queue<MetaInvokeBasedAsyncQDebugPrinter::PrinterMessage> MetaInvokeBasedAsyncQDebugPrinter::takeTaskQueue() { std::queue<PrinterMessage> buffer; std::lock_guard locker{ m_mutex }; m_messages.swap(buffer); return buffer; }
На самом деле это — отличный подход. В отличие от варианта с сигналами, он не содержит лишних сущностей, и при этом делает всё тоже самое.
Немного поиграемся
А теперь почитаем документацию, и увидим, что QMetaObject::invokeMethod позволяет передавать параметры методов, что очень удобно для нас.
И если искушённые читатели думают, что мы сейчас опустимся в развитии до уровня Qt4, и будем использовать строковые литералы и макросы Q_ARG, то они правы. К сожалению, нормального способа передавать аргументы я не знаю, поэтому воспользуемся несколько некрасивым, зато рабочим и лаконичным.
Это позволит нам в принципе отказаться от очереди, но т.к. вся метаобъектная система Qt построена на десятках тысяч копирований (предполагается, что передаваемые типы либо лёгкие, либо обладают cow-оптимизацией), то небходимо будет обернуть некопируемый PrinterMessage в std::shared_ptr, дабы тот мог быть передан посредством мета-вызова.
.h-файл
class MetaInvokeBasedAsyncQDebugPrinter : public QObject { private: Q_OBJECT class PrinterMessage { static const bool m_isMetatypeRegistred; /**/ }; public: explicit MetaInvokeBasedAsyncQDebugPrinter(QObject *parent = nullptr); QFuture<void> print(const QString& message); private slots: void handleNextMessage(std::shared_ptr<PrinterMessage> message); };
Отметим, что интерфейс класса сильно упростился. Теперь в нём только конструктор, метод print, добавляющий задание на вывод и метод handleNextMessage, это задание выполняющий.
Также в классе PrintMessage добавляется статическое поле m_isMetatypeRegistred. Это поле нужно, чтобы зарегистрировать std::shared_ptr<PrinterMessage> как тип, доступный для передачи в метасистеме.
Регистрация метатипа std::shared_ptr<PrinterMessage>
const bool MetaInvokeBasedAsyncQDebugPrinter::PrinterMessage::m_isMetatypeRegistred = []() -> bool { qRegisterMetaType<std::shared_ptr<MetaInvokeBasedAsyncQDebugPrinter::PrinterMessage>>("std::shared_ptr<PrinterMessage>"); return true; }();//тут же происходит вызов лямбды
Эта регистрация строится на основе того, что статические поля инициализируются один, и ровно один раз.
Реализация Active object
MetaInvokeBasedAsyncQDebugPrinter::MetaInvokeBasedAsyncQDebugPrinter(QObject *parent) :QObject{ parent } {} QFuture<void> MetaInvokeBasedAsyncQDebugPrinter::print(const QString &message) { auto task = std::make_shared<PrinterMessage>(message); auto future = task->promise().future(); QMetaObject::invokeMethod(this, "handleNextMessage", Qt::ConnectionType::QueuedConnection, Q_ARG(std::shared_ptr<PrinterMessage>, task)); return future; } void MetaInvokeBasedAsyncQDebugPrinter::handleNextMessage(std::shared_ptr<PrinterMessage> message) { qDebug() << message->message(); message->promise().finish(); }
Как видим, всё стало куда проще.
1. Конструктор просто задаёт родителя.
2. handleNextMessage обрабатывает задачу (исключён этап работы с очередью задач)
3. print создаёт задачу, достаёт future на неё, и вызывает QMetaObject::invokeMethod.
Заключение
Таким образом мы пришли к одной из самых лаконичных реализаций Active object, возможных в Qt.
Это не самая быстрая реализация, как раз за счёт копирований std::shared_ptr в разных потоках, что будет дёргать кучу атомарных переменных с семантикой acquire/release.
Но тем не менее, этой реализации вам более чем хватит для абсолютно любых прикладных задач, где необходим Active object.
Можно дополнительно покопаться в мета-системе Qt, доставать метод-обработчик в классе QMetaMethod, и вызывать его там. Но лично я считаю, что это лишнее. Да и от использования строковых литералов это нас не избавит.
ссылка на оригинал статьи https://habr.com/ru/post/712328/
Добавить комментарий