Thinking about Active Object in the context of Qt6. Part 2
Links to articles
Foreword
It’s time to write the second part of the article. This time we’ll look at something you’re likely to come up with when working on multi-threaded code using Qt.
Again, I highly recommend reading this article. It provides an excellent layer of understanding of how Qt works and is essential for the examples in this article.
What is the idea
If we recall the example from the first part of the cycle, then we can say that there is almost always a queue between the client and the active object (in practice, I have not seen an active object without queues). But if you read this article, then we learn that inside each Qt event loop lies a queue.
Thus, if you wrap the task in a QEvent class heir, then you can easily use this queue to transfer tasks. So we also get the opportunity to set the priority of message transmission.
Out of the box, Qt has Qt::HighEventPriority(1), Qt::NormalEventPriority(0), and Qt::LowEventPriority(-1), but in fact you can pass any numeric value within an int32_t.
Implementing an Event-Oriented Active Object
First, let’s create an event class. It will be a private inner class for our Active object. Those. no one from the outside will be able to see the type of this event.
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;
}
The event class is extremely simple. We take advantage of the fact that QEvent is a move-only class, which means it can pass QPromise internally by value.
Further, the entire QEvent class is reduced to a simple DTO with two getters (for a message and a promise).
In the constructor, you must specify QEvent::Type greater than or equal to QEvent::Type::User. This is necessary so that Qt will automatically pass this event to the customEvent() virtual method.
Now let’s create the Active object class itself. It will have the same print method that returns a future as in the example from the first part.
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();
}
}
There is the same print method that returns a future as in the example from the first part. In this method, the active object sends an event to itself.
This is necessary because the print method can be called from any thread, but the customEvent method that handles the event will be called on the thread that owns this QObject.
One thing to note here is the fact that the sendEvent method uses QCoreApplication::notify which is not thread safe. Therefore, if you are not sure that the sender and receiver of the event are on the same thread (and here we are sure they are), then use the postEvent method.
Using such a class is very similar to using the class from the example of the last part:
Application of 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");
});
Here, a printer (our Active object) is created, which is then moved by moveToThread() to a separate thread created specifically for it. After that, the thread is launched (you can move it after launch, the effect will not change).
It should be understood that this Active object is asynchronous, unlike the previous one. This means that it is generally not necessary to use a separate thread for it. But much more often, you will use this property for another purpose: to create a separate thread that will process several Active objects at the same time, performing some kind of highly loaded tasks that would otherwise simply block the main thread.
pros
Sufficiently stable implementation, which often, with minimal modification, will cover all your needs in general. Isn’t this happiness.
Making the most of built-in Qt mechanisms.
To remove all PrinterMessageEvents from the event queue, just yank QCoreApplication::removePostedEvents(printer, PrinterMessageEvent::Type);
Minuses
Any tactical genius can put an event filter on this Active object and block all events. But this is not scary, because this way you can break the whole Qt in general.
There is no built-in mechanism to stop receiving incoming messages. You can put in a std::atomic_flag which will solve all your problems. Moreover, this can just be done through event-filter (which sometimes allows you to beautifully block messages for active objects without changing them themselves). But he’s not really needed.
You must carefully document all event classes that you create in your code. The reason for this is QEvent::Type, which must be different for all event classes. At least because of the QCoreApplication::removePostedEvents method, which focuses on this type (although you will use it a little less often than never).
Conclusion
This version of Active object can be fully used in real code. In the future, we will look at more realistic examples of using the pattern, which can be applied in the code “as is”.
The source code is on GitHub.