mafulechkaOct. 13, 2020, 2:57 a.m.

Asynchronous APIs in Qt 6

As readers may already know, Qt provides several threading constructs (threads, mutexes, wait states, etc.) as well as higher-level APIs such as QThreadPool, Qt Concurrent, and other related classes. This article will cover the higher level asynchronous APIs and the changes introduced in Qt 6.

Higher level concurrency API in Qt

Qt Concurrent simplifies multi-threaded programming by eliminating the need for low-level synchronization (primitives such as mutexes and locks) and manages multiple threads manually. It provides matching, filtering, and reduction algorithms (better known from functional programming) for processing repetitive containers in parallel. In addition, there are classes such as QFuture, QFutureWatcher, and QFutureSynchronizer for accessing and monitoring the results of asynchronous computations. While they are all very useful, there were still some drawbacks such as the inability to use QFuture outside of Qt Concurrent, the lack of support for combining multiple calculations for simpler and cleaner code, the lack of flexibility of the Qt Concurrent API, etc. As of Qt 6, the developers We tried to take into account the feedback received over the past years and make multi-threaded Qt programming more enjoyable and fun!

Attaching continuations to QFuture

A common scenario in multi-threaded programming is to perform an asynchronous computation, which in turn must call another asynchronous computation and pass data to it that depends on the other, and so on. Since each stage requires the results of the previous one, you either need to wait (by blocking or polling) until the previous stage completes and use its results, or structure your code in a "call-callback" style. None of these options are perfect: you either end up wasting resources waiting, or you end up with complex, unmaintainable code. Adding new steps or logic (for error handling, etc.) further increases the complexity.

To better understand the problem, consider the following example. Let's say you want to download a large image from the web, do some heavy processing on it, and display the resulting image in your application. So, you have to do the following steps:

• make a network request and wait for all data to be received;
• create an image from the raw data;
• process the image;
• show an image.

There are following methods for each step that need to be called sequentially:

QByteArray download(const QUrl &url);
QImage createImage(const QByteArray &data);
QImage processImage(const QImage &image);
void show(const QImage &image);

You can use QtConcurrent to run these tasks asynchronously and QFutureWatcher to track progress:

void loadImage(const QUrl &url) {
    QFuture data = QtConcurrent::run(download, url);
    QFutureWatcher dataWatcher;
    dataWatcher.setFuture(data);

    connect(&dataWatcher, &QFutureWatcher ::finished, this, [=] {
        // handle possible errors
        // ...
        QImage image = createImage(data);
        // Process the image
        // ...
        QFuture processedImage = QtConcurrent::run(processImage, image);
        QFutureWatcher<QImage> imageWatcher;
        imageWatcher.setFuture(processedImage);

        connect(&imageWatcher, &QFutureWatcher::finished, this, [=] {
            // handle possible errors
            // ...
            show(processedImage);
        });
    });
}

Doesn't look good, does it? The logic of the application is mixed with the boilerplate code needed to chain the program components together. And you know that the more steps you add to the chain, the scarier it gets. QFuture helps solve this problem by adding support for attaching continuations via the QFuture::then() method:

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show);

It certainly looks much better! But one thing is missing - error handling. You can do something like:

auto future = QtConcurrent::run(download, url)
            .then([](QByteArray data) {
                // handle possible errors from the previous step
                // ...
                return createImage(data);
            })    
            .then(...)    
            ...

This will work, but the error handling code is still mixed up with program logic. It's also probably not worth running the whole chain if one of the steps fails. This can be solved with the QFuture::onFailed() method, which allows you to attach specific error handlers for each possible type of error:

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show)
            .onFailed([](QNetworkReply::NetworkError) {
                // handle network errors
            })
            .onFailed([](ImageProcessingError) {
                // handle image processing errors
            })
            .onFailed([] {
                // handle any other error
            });

Note that exceptions must be enabled in order to use .onFailed() . If any of the steps fail with an exception, the chain is broken and the error handler corresponding to the type of exception thrown is invoked.

Like .then() and onFailed(), there is also .onCanceled() to attach handlers in case the future is cancelled.

Create a QFuture from signals

Like futures, signals are also something that will become available sometime in the future, so it seems natural to be able to work with them like futures, attach continuations, failure handlers, etc. Given a QObject-based MyObject class with a void signal mySignal(int), you can use this signal as a future like this:

QFuture intFuture = QtFuture::connect(&object, &MyObject::mySignal);

You can now attach continue, fail, or cancel handlers to the received future.

Note that the type of the resulting future is the same as the type of the signal argument. If it has no arguments, a QFuture is returned . In the case of multiple arguments, the result is stored in a std::tuple.

Let's go back to the first step (i.e. loading) of your image processing example to see how this can be useful in practice. There are many ways to implement this, let's use QNetworkAccessManager to send a network request and receive data:

QNetworkAccessManager manager;    
...

QByteArray download(const QUrl &url) {        
    QNetworkReply *reply = manager.get(QNetworkRequest(url));
    QObject::connect(reply, &QNetworkReply::finished, [reply] {...});

    // wait until we've received all data
    // ...    
    return data;        
}

But waiting for the lock above is not good, it would be better if we could get rid of that and instead say "when QNetworkAccessManager gets the data, create an image, then process it and then show" image, then process it and display"). You can do this by connecting the network access manager's finished() signal to the QFuture:

QNetworkReply *reply = manager.get(QNetworkRequest(url));

auto future = QtFuture::connect(reply, &QNetworkReply::finished)
        .then([reply] {
            return reply->readAll();
        })
        .then(QtFuture::Launch::Async, createImage)
        .then(processImage)
        .then(show)        
        ...

You may notice that instead of using QtConcurrent::run() to asynchronously load and return data on a new thread, we simply connect to the QNetworkAccessManager::finished() signal, which starts the computation chain. Also notice the extra parameter on the next line:

 .then(QtFuture::Launch::Async, createImage)

By default, continuations attached with .then() are called on the same thread that the parent thread was running on (in your case, the main thread). Now that we are not using QtConcurrent::run() to start the chain asynchronously, we need to pass an additional parameter to QtFuture::Launch::Async to run the continuation chain on a separate thread and avoid blocking the UI.

Creating a QFuture

Until now, the only "official" way to create and store a value inside a QFuture was to use one of the QtConcurrent. So outside of QtConcurrent, QFuture wasn't very useful. Qt 6 finally has a "setter" analogue of QFuture: QPromise introduced by Andrey Golubev. It can be used to set values, progress, and exceptions for asynchronous calculations, which can later be accessed via QFuture. To demonstrate how this works, let's rewrite the image processing example again and use the QPromise class:

QFuture download(const QUrl &url) {
    QPromise promise;
    QFuture future = promise.future();

    promise.reportStarted(); // notify that download is started

    QNetworkReply *reply = manager.get(QNetworkRequest(url));
    QObject::connect(reply, &QNetworkReply::finished,
            [reply, p = std::move(promise)] {
                p.addResult(reply->readAll());
                p.reportFinished(); // notify that download is finished
                reply->deleteLater();
            });

    return future;
}
auto future = download()
        .then(QtFuture::Launch::Async, createImage)
        .then(processImage)
        .then(show)
        ...

Changes in QtConcurrent

Thanks to Jarek Kobus, Marten Nordheim, Carsten Heimrich, Timur Pocheptsov and Vitaly Fanaskov, QtConcurrent has also received some nice updates. The existing APIs have received some improvements, in particular:

  • You can now set your own thread pool for all QtConcurrent methods instead of always running them in the global thread pool and potentially blocking other tasks from executing.
  • Map object reduction and filtering algorithms can now take an initial value, so you don't have to look for workarounds for types that don't have a default constructor.
  • QtConcurrent::run has been improved to work with variadic arguments and move-only types.

In addition, two new APIs have been added to QtConcurrent to give users more flexibility. Let's look at them in more detail.

QtConcurrent::runWithPromise

The new QtConcurrent::runWithPromise() method developed by Jarek Kobus is another nice addition to the QtConcurrent framework. It is very similar to QtConcurrent::run(), except that it makes the QPromise object associated with the given task available to the user. This is achieved by having the runnable passed to QtConcurrent::runWithPromise() take a reference to the QPromise object:

auto future = QtConcurrent::runWithPromise(
            [] (QPromise &promise, /* other arguments may follow */ ...) {
                // ...
                for (auto value : listOfValues) {
                    if (promise.isCanceled())
                        // handle the cancellation

                // do some processing...

                promise.addResult(...);
                promise.setProgressValue(...);
                }
            },
            /* pass other arguments */ ...);

As you can see, with runWithPromise(), users have more control over the task and can respond to cancel or suspend requests, report progress, etc., which is not possible with QtConcurrent::run().

QtConcurrent::task

QtConcurrent::task() provides a loose interface for executing a task on a separate thread. This is a more modern alternative to QtConcurrent::run() that allows you to more conveniently set up tasks. Instead of using one of the few QtConcurrent::run() reloads to pass the parameters to run the task, you can specify them in any order, skip those you don't need, and so on. For example:

QFuture future = QtConcurrent::task(doSomething)
        .withArguments(1, 2, 3)
        .onThreadPool(pool)
        .withPriority(10)
        .spawn();

Note that, unlike run(), you can also pass a task priority.

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.
Support the author Donate
M
  • #
  • Feb. 17, 2022, 8:22 a.m.

Не раскрыт один момент. Как после всей цепочке отдать результат в gui поток? Вот пример из статьи:

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(show);

Допустим в методе show() мы вставляем изображение в лейбл. Но вот show же выполняется в другом не в gui потоке.
Чтобы show выполнялся в gui потоке, мне надо делать так?

auto future = QtConcurrent::run(download, url)
            .then(createImage)
            .then(processImage)
            .then(QtFuture::Launch::Sync, show);

Comments

Only authorized users can post comments.
Please, Log in or Sign up
Card image cap
Pulsum Via

Project for travelers from EVILEG.

Go
Fornex

Let me recommend you a great European Fornex hosting.

Fornex has proven itself to be a stable host over the years.

For Django projects I recommend VPS hosting

Following the link you will receive a 5% discount on shared hosting services, dedicated servers, VPS and VPN

View Hosting
Share on social networks
Donate

The EVILEG project has switched to a non-commercial basis and will develop solely on the enthusiasm of the site creator, the enthusiasm of users, donations and the hosting referral system

Thank you for your support

Available ways to support the project

PayPal

PatreonYandex.MoneyMore

C++ - Test 001. The first program and data types

  • Result:80points,
  • Rating points4

C++ - Test 001. The first program and data types

  • Result:66points,
  • Rating points-1
GR

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:90points,
  • Rating points8
Last comments

Qt/C++ - Lesson 061. Adding images to the application using the Drag And Drop method from the file manager

Доброго времени суток. А если нужно и изображение и текст? Что-то потерялся немного.... // Вместо отрисовки иконки и текста будем отрисовывать только одно изображение // с н…
AS

Qt/C++ - Lesson 004. QSqlTableModel – How to present the table from database?

error insert into TableExample " Количество параметров не совпадает" Я путь свой прописывала и даже бд удаляла, чтобы заново сделать, не работает. (всё остальное как у вас... Вроде ка…
i

Qt/C++ - Lesson 042. PopUp notification in the Gnome style using Qt

cialis powder comprare cialis online
LD

GameDev on Qt - Tutorial 1. Track mouse movement in QGraphicsScene

Вполне возможно, что ты не закинул graphicsView в дизайнере в виджет
LD

GameDev on Qt - Tutorial 1. Track mouse movement in QGraphicsScene

Кому интересно, поворот в slotTarget можно в одну строку организовать this->setRotation(90 + rotation() + qRadiansToDegrees(qAtan2(mapFromScene(point).y(), mapFromScene(point).x())));
Now discuss on the forum
M

Sorting the added QML elements in the ListModel

legal online pharmacy
  • Nomad
  • July 30, 2022, 5:42 a.m.

Как работать с HTMX?

Приветствую колеги. На днях наткнулся на вот это : https://htmx.org/ На офф сайте написанно вот такая фраза: htmx gives you access to AJAX, CSS Transitions, We…
h
  • harisr
  • July 25, 2022, 2:56 a.m.

QT - Native App Integration

Привет, у нас уже есть собственное приложение для Android. Можем ли мы интегрировать пользовательское представление QT в приложение со всем приложением QT внутри представления. Если да, ука…

Правильный запуск сервера на vps - Django

О я как то себе дома локальный сервер создавал. Вам же нужно просто сделать ручками конфигурацию системы. Настроить Nginx ну либо Apache (тут кому что нравится). Соответственно БД и всё остально…
o

Распознание объектов

Я к тому, что, возможно, софт уже есть.
About
Services
© EVILEG 2015-2022
Recommend hosting TIMEWEB