Thinking about Active Object in the context of Qt6. Part 1


Foreword

It’s four in the morning outside. In my heart I don’t understand why I am writing this, what I want to achieve with this, etc. In short, this will be a series of articles from the category of “hoba as I can”, and this very “hoba” is often too obvious and elementary, and far from always useful, especially in the context of the Qt philosophy. So it will just be “thinking about everything”.

What are we going to do here?

Consider the implementation of the Active Object pattern in C ++ with various gadgets from Qt. First, I’ll dig into a couple of hellowords, then we’ll move on to something at least somewhat usable in the context of real code.

I will use Qt6.4 because it has a lot of cool stuff that Qt5 didn’t have (at least QPromise and QHttpServer that everyone has been waiting for), but using some Qt6 feature, I will try to give an alternative for Qt5.

A little about the patient

Active object is a multi-threaded programming pattern, the main task of which is to separate the call of some operation and its execution. If it’s simple: one thread pulls the function, but perhaps the same one, perhaps another thread, will be engaged in its execution. In this case, the executor thread executes all requests sequentially, which eliminates any need to synchronize access.

In most cases, the entire Active object comes down to two approaches: future + continuations or just callbacks if there is no normal future at hand (hello, C ++).

It should be noted that some of the things that will be described here are not very similar to the canonical implementation of the pattern. The reason for this is simple: the asynchronous nature of Qt, which often saves a lot of headaches when synchronizing threads. But, as I noted above, the entire series of articles will be just a set of my thoughts and experiments on this topic.

First pancake

Where should you start? That’s right: you should always start with a bad example. To begin with, let’s depict the simplest scheme for implementing an Active object.

Primitive Active object
Primitive Active object

The work is as follows: the Client goes to the Active object and asks it to perform some task. The Active object gives the Client a future for the result of the task, and puts the task in the queue. After that, the executor thread takes the task from the queue, executes it, and writes the result to the promise.

Let’s start with the simplest, and at the same time bad, implementation that you can think of: inherit from the QThread class, and override the run method. Why this method is bad, I will explain after implementation.

Our first Active object won’t do anything fancy. For starters, let it simply print messages to the console and notify the person who sent the message that it has been printed.

Instead of QPromise from Qt6, you can take the library QtPromise. Divine thing, pretty nice interface, a lot of useful gadgets.

AsyncQDebugPrinter.h
class AsyncQDebugPrinter : public QThread {
private:
    class PrinterMessage {
    private:
        QPromise<void> m_promise;
        const QString m_message;
    public:
        PrinterMessage(const QString &message);
        const QString& message() const;
        QPromise<void>& promise();
    };
private:
    std::queue<PrinterMessage> m_messages;
    std::condition_variable m_messagesAwaiter;
    std::mutex m_mutex;
public:
    explicit AsyncQDebugPrinter(QObject *parent = nullptr);
    QFuture<void> print(const QString& message);
protected:
    virtual void run() override;
};

The PrinterMessage class internally encapsulates a message and a promise, setting which will notify the sender of the message that it has been printed. Because Since it contains within itself an instance of the move-only class QPromise, then it is itself a move-only class.

Implementing PrinterMessage
AsyncQDebugPrinter::PrinterMessage::PrinterMessage(const QString &message)
    :m_message{ message } {}
const QString& AsyncQDebugPrinter::PrinterMessage::message() const {
    return m_message;
}
QPromise<void> &AsyncQDebugPrinter::PrinterMessage::promise() {
    return m_promise;
}

The implementation of AsyncQDebugPrinter is fairly trivial:

The print method takes a message, creates a task to output it (task) and takes a future from this task. It then locks the mutex, moves task to the message queue, notifies condition_variable that a new message has arrived, and returns a future to the caller, after which the mutex is unlocked.

print method
//AsyncQDebugPrinter.cpp
AsyncQDebugPrinter::AsyncQDebugPrinter(QObject *parent)
    :QThread{ parent } {}

QFuture<void> AsyncQDebugPrinter::print(const QString &message) {
    auto task = PrinterMessage{ message };
    auto future = task.promise().future();
    std::lock_guard locker{ m_mutex };
    m_messages.push(std::move(task));
    m_messagesAwaiter.notify_one();
    return future;
}

In the run method, the executor thread spins. It spins until someone from the outside pulls the requestInterruption () method of the QThread class.

All the work of the method is to ensure that condition_variable waits until new messages appear in the queue. Further, this queue is moved to the buffer, after which the lock is released. This is an important point: there is a lock only when working with the queue, while it is minimal, and there are no locks in principle when processing messages.

Well, then it goes through all the messages in the queue, the text is displayed on the screen, and the promise is set to the finished state.

run method
//AsyncQDebugPrinter.cpp
void AsyncQDebugPrinter::run() {
    while(not isInterruptionRequested()) {
        std::unique_lock<std::mutex> locker{ m_mutex };
        if(m_messagesAwaiter.wait_until(locker, std::chrono::steady_clock::now() + 500ms, [this]() ->bool { return not m_messages.empty(); })) {
            auto buffer = std::move(m_messages);
            locker.unlock();
            
            while(not buffer.empty()) {
                qDebug() << buffer.front().message();
                buffer.front().promise().finish();
                buffer.pop();
            }
        }
    }
}

Trying to use

Launching this is pretty easy. We create an object, pull the start method, and enjoy the opportunity to do print(“Hello, world”) from different threads.

qDebug() << "Start application";//А вот тут у нас будет id главного потока
auto printer = new AsyncQDebugPrinter{ qApp };
printer->start();

printer->print("Hello, world!").then([printer] {
    qDebug() << "In continuation";
    printer->print("Previous message was printed");
});

The output will be an oil painting:

The numbers 16036 and 16037 are, in fact, the id of the threads. It can be seen that “Start application” is displayed in the main thread, but the rest of the messages are already in the Active object executor thread.

And here it should be noted that the continuation (the lambda thrown into .then) is also executed by the active object thread. This means that he is not doing his job.

Sure, you can just make cheap continuations that just start new tasks, but that’s not the way of the Jedi. We’ll do okay. Let’s slightly correct the code: add the following thing to .then as the first parameter (before the lambda):

    printer->print("Hello, world!").then(QtFuture::Launch::Async, [printer] {
        qDebug() << "In continuation";
        printer->print("Previous message was printed");
    });

QtFuture::Launch::Async will make the lambda execute on QThreadPool::globalInstance() threads, and the Active object will be able to do only what we created it for.

Now here’s another thing:

  • 16249 – main thread

  • 16250 – active object stream

  • 16251 – Thread from QThreadPool::globalInstance()

Don’t try to create any QObject in continuations that run on QThreadPool threads. These threads do not conduct events, which means that any QObject will be dead (neither signals nor events will work). By the way, it is also impossible to create a QObject in continuations executed on the thread of this Active object, because it will not provide any events in the same way.

So why is this implementation bad?

Let’s start by saying that inheriting from QThread is a bad idea. This is detailed here.

The quit and exit methods will not work, because we didn’t start the QEventLoop in the run method. A class with a non-working interface – what could be worse?

If you do not call requestInterruption () and do not wait for the end of the thread execution before deleting the executor, then when its destructor is called, terminate will be called and the user will see an ugly error message. On the other hand, you can add a forced wait in the destructor of the Active Object, which will solve this problem.

Conclusion

The first article turned out to be introductory and has not yet revealed the advantages of Active object. I’ll try to fix this later.

The source code of the example is available at GitHub.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *