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
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.
Не раскрыт один момент. Как после всей цепочке отдать результат в gui поток? Вот пример из статьи:
Допустим в методе show() мы вставляем изображение в лейбл. Но вот show же выполняется в другом не в gui потоке.
Чтобы show выполнялся в gui потоке, мне надо делать так?