Asynchronous APIs in Qt 6
September 16, 2020 by Sona Kurazyan | Comments
As readers may already know, Qt provides several multithreading constructs (threads, mutexes, wait conditions, etc.), and higher level APIs like QThreadPool
, Qt Concurrent and other related classes. Those who are not familiar with Qt threading support yet or want to learn more, can participate in our online training sessions . In this post we will concentrate on higher level asynchronous APIs and changes introduced in Qt 6.
Higher level concurrency APIs in Qt
Qt Concurrent makes multithreaded programming easier by eliminating the need for low-level synchronization (primitives, such as mutexes and locks) and managing multiple threads manually. It provides map, filter and reduce algorithms (better known from functional programming) for parallel processing of iterable containers. In addition, there are classes like QFuture
, QFutureWatcher
and QFutureSynchronizer
to access and monitor the results of asynchronous computations. Although all of these are very useful, there were still some drawbacks, like inability to use QFuture
outside Qt Concurrent, lacking support for chaining multiple computations for simpler and cleaner code, lack of flexibility of Qt Concurrent APIs, etc. For Qt 6 we tried to address the feedback gathered over the past years and make multithreaded programming with Qt more enjoyable and fun!
Attaching continuations to QFuture
A common scenario in multithreaded programming is running an asynchronous computation, which in turn needs to invoke another asynchronous computation and pass data to it, which depends on another one, and so on. Since each stage requires the results of the previous one, you need to either wait (by blocking or polling) until the previous stage completes and use its results, or structure your code in "call-callback" fashion. None of these options is perfect: you either waste resources on waiting, or get complex unmaintainable code. Adding new stages or logic (for error handling, etc.) increases the complexity even further.
To get a better understanding of the problem, let's consider the following example. Let's say we want to download a big image from network, do some heavy processing on it and show the resulting image in our application. So we have the following steps:
- make a network request and wait until all data is received
- create an image from the raw data
- process the image
- show it
And we have the following methods for each step that need to be invoked sequentially:
QByteArray download(const QUrl &url);QImage createImage(const QByteArray &data);QImage processImage(const QImage &image);void show(const QImage &image);
We can use QtConcurrent to run these tasks asynchronously and QFutureWatcher
to monitor the progress:
void loadImage(const QUrl &url) {
QFuture<QByteArray> data = QtConcurrent::run(download, url);
QFutureWatcher<QByteArray> dataWatcher;
dataWatcher.setFuture(data);
connect(&dataWatcher, &QFutureWatcher<QByteArray>::finished, this, [=] {
// handle possible errors
// ...
QImage image = createImage(data);
// Process the image
// ...
QFuture<QImage> processedImage = QtConcurrent::run(processImage, image);
QFutureWatcher<QImage> imageWatcher;
imageWatcher.setFuture(processedImage);
connect(&imageWatcher, &QFutureWatcher<QImage>::finished, this, [=] {
// handle possible errors
// ...
show(processedImage);
});
});
}
Doesn't look nice, does it? The application logic is mixed with the boilerplate code required to link things together. And you just know it's going to get uglier, the more steps we add to the chain. QFuture
helps to solve this problem by adding support for attaching continuations via QFuture::then()
method:
auto future = QtConcurrent::run(download, url)
.then(createImage)
.then(processImage)
.then(show);
This undoubtedly looks much better! But one thing is missing: the error handling. You could 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 with program logic. Also we probably don't want to run the whole chain if one of the steps has failed. This can be solved by using QFuture::onFailed()
method, which allows us to attach specific error handlers for each possible error type:
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 using .onFailed()
requires exceptions to be enabled. If any of the steps fails with an exception, the chain is interrupted, and the error handler matching with the exception type that was thrown is called.
Similar to .then()
and .onFailed()
, there is also .onCanceled()
, for attaching handlers in case the future got canceled.
Creating QFuture from signals
Similar to futures, signals also represent something that will become available sometime in the future, so it seems natural to be able to work with them as with futures, attach continuations, failure handlers, and so on. Given a QObject
-based class MyObject
with a signal void mySignal(int)
, you can use this signal as a future in the following way:
QFuture<int> intFuture = QtFuture::connect(&object, &MyObject::mySignal);
Now you can attach continuations, failure or cancellation handlers to the resulting future.
Note that the type of the resulting future matches with the argument type of the signal. If it has no arguments, then QFuture<void>
is returned. In case of multiple arguments, the result is stored in a std::tuple
.
Let's go back to the first (i.e. download) step of our image processing example, to see how this can be useful in practice. There are many ways to implement it, we will use QNetworkAccessManager
to send the network request and get the 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 the blocking wait above is not good, it would be better if we could get rid of it, and instead say "when QNetworkAccessManager
gets the data, create an image, then process it and then show". We can do it by connecting the network access manager's finished()
signal to 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 can notice that now instead of using QtConcurrent::run()
to asynchronously download and return the data in a new thread, we are simply connecting to the QNetworkAccessManager::finished()
signal, which starts the chain of computations. Also note the additional parameter in the following line:
.then(QtFuture::Launch::Async, createImage)
By default continuations attached by .then()
are invoked in the same thread in which the parent has been running (the main thread in our case). Now that we don't use QtConcurrent::run()
to asynchronously launch the chain, we need to pass the additional QtFuture::Launch::Async
parameter, to launch the chain of continuations in a separate thread and avoid blocking the UI.
Creating a QFuture
So far the only "official" way of creating and storing a value inside QFuture
was using one of the methods of QtConcurrent
. So outside QtConcurrent
, QFuture
was not very useful. In Qt 6 we finally have the "setter" counterpart of QFuture
: QPromise
, introduced by Andrei Golubev. It can be used to set values, progress and exceptions for an asynchronous computation, which can be later accessed via QFuture
. To demonstrate how it works, let's rewrite the image processing example again, and make use of the QPromise
class:
QFuture<QByteArray> download(const QUrl &url) {
QPromise<QByteArray> promise;
QFuture<QByteArray> future = promise.future();
promise.start(); // 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.finish(); // 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, Mårten Nordheim, Karsten Heimrich, Timur Pocheptsov and Vitaly Fanaskov, QtConcurrent also has received nice updates. The existing APIs got some improvements, in particular:
- You can now set a custom thread pool to all methods of QtConcurrent, instead of always running them on the global thread pool and potentially blocking the execution of other tasks.
- Map and filter reduce algorithms can now take an initial value, so you don't have to do workarounds for types that don't have a default constructor.
- QtConcurrent::run
has been improved to work with a variable number of arguments and move-only types.
Additionally, we've added two new APIs to QtConcurrent to give more flexibility to the users. Let's look at those in more detail.
QtConcurrent::run with promise
Thanks to Jarek Kobus, the QtConcurrent::run()
method has been improved to provide access to the promise object associated with the given task inside the run()
method. This allows the users to do progress reporting, report multiple results, suspend or cancel the execution if it was requested. This is achieved by making the runnable passed to QtConcurrent::run()
accept a reference to the QPromise
object:
auto future = QtConcurrent::run(
[] (QPromise<T> &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, in this mode the users have more control over the task, and can react to cancellation or suspension requests, do progress reporting, etc., which was not possible before.
QtConcurrent::task
QtConcurrent::task()
provides a fluent interface for running a task in a separate thread. It is a more modern alternative for QtConcurrent::run()
, and allows configuring the tasks in a more convenient way. Instead of using one of the few overloads of QtConcurrent::run()
to pass the parameters for running a task, you can specify them in any order, skip the ones that are not needed, and so on. For example:
QFuture<int> future = QtConcurrent::task(doSomething)
.withArguments(1, 2, 3)
.onThreadPool(pool)
.withPriority(10)
.spawn();
Note that, unlike run(), you can also pass a priority for the task.
If you find the new functionality interesting, you can also check the Qt 6 documentation snapshots for more details and examples. And please don't hesitate to leave feedback!
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.9 Now!
Download the latest release here: www.qt.io/download.
Qt 6.9 is now available, with new features and improvements for application developers and device creators.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.
Commenting for this post has ended.
These are great news!
This is great indeed! QtConcurrent in Qt5 is a good improvement on what's available in C++ and this improves it even further.
Seems very similar https://qtpromise.netlify.app/qtpromise/getting-started.html Making a promise from signal is very useful
Another feature that AsyncFuture has that this implementation is lacking is a way to combine multiple QFutures into one future. For example:
QFuture<int> futureFunc1 = QtConcurrent::run(func1);
QFuture<int> futureFunc2 = QtConcurrent::run(func2);
QFuture<void> futureSignal = QtFuture::connect(button, &QButton::onClicked);
//Combine multiple futures and sync when all of them are finished
QFuture<void> finalFuture = QtFuture::combine({futureFunc1, futureFunc2, futureSignal});
finalFuture.then([futureFunc1, futureFunce2](){ qDebug() << "Results func1:" << futureFunc1.result() << "Result func2:" << futureFunc2.result();});
The current implementation doesn't allow for parallel pipelines to be merged into one QFuture.
Or is this supported?
You're right, it's not supported at the moment. Support for combining multiple futures is also one of the things we can consider adding in future.
I added a bug report here: https://bugreports.qt.io/browse/QTBUG-86728
Any plans for a QCoroutine? :)
No plans for it yet :)
This looks really nice and is well thought out. I've been using AsyncFuture library for a while now and it filled in missing QFuture functionality for Qt5. Couple questions since the Qt6 snapshot documentation doesn't appear to have this new API published:
Can you dynamically chain QFuture? QFuture future = QtConcurrent::task(doSomething) QFuture chainFuture; if(...) { chainFuture = future.then(func1); } else { chainFuture = future.then(func2); } chainFuture.then(finalFunc);
It looks like QFuture isn't templated anymore. Does it still support a list of results? I always thought this was a bit confusing. I guess the new QFuture api isn't compatible with the Qt5 line?
Can you set QFuture (or QPromise) with a completed value. Surprisingly this is very useful. Sometimes you know the value and don't need to reprocess.
Can the .then() statements return QFuture? For example if you have QtConncurrent::mapped() run inside of a .then(QtFuture::Launch::Async). This is something I do a lot in AsyncFuture.
Thank you for the feedback! I fixed the link to documentation snapshots, it was pointing to a wrong version before. And QFuture docs can be found here.
Regarding to your questions:
Yes, your example is completely valid.
QFuture is actually still templated and is entirely compatible with QFuture in Qt 5. And it still supports multiple results, because that's useful with QtConcurrent and in other scenarios, for example, progress reporting, etc.
Unfortunately there is no way of creating a completed QFuture/QPromise yet, but I agree with you, that could be very useful. Will consider implementing it in the upcoming releases.
If the callable passed to .then() returns a QFuture, then the return type of .then() will be QFuture<QFuture<T>>, i.e. no unwrapping is implemented yet. But again, that's also something we'll consider adding :)
Awesome! Thanks for the quick feedback.
I've made bug reports for 3. https://bugreports.qt.io/browse/QTBUG-86724 4. https://bugreports.qt.io/browse/QTBUG-86725
I didn't really study all these old and new APIs, BUT as a Qt user I honestly don't get all the complexities. Can't we just a have a Promise object like in Javascript? I don't understand why QFuture exists in the first place...These APIs look more complicated than signals connected to lambdas so they defeat their own purpose.
I understand this is about multithreading. The confusione arises from talking about "asynchronous" instead of "multithreaded" . Multithreading is 1% of asynchronous use cases. It's needed only for heavy number crunching, not for network requests as in this post examples. What is really missing is a very simple QPromise class, very much like Javascript's. Not a multithreaded one. A function returning a QPromise could be used like this:
myPromiseFunction(myArgs) .then([](auto &result) { // do something with result }).error([](auto &e) { // handler error });
I'm not familiar with Javascript's Promise, but don't see how the separation of producer (i.e. QPromise) and consumer (i.e. QFuture) interfaces makes things complex. The idea is that you can have one producer, and multiple consumers (possibly in different threads) independent from it. Many libraries have this separation (including STL, boost, etc.)
This is not only about multithreading, the new functionality is useful for any asynchronous computation. I think, the example with network request demonstrates that.
Well, you can write exactly the same example with a function returning a QFuture, I don't see what's the problem.
Hey Sona, thanks for the work, it looks very promising! (pun intended). In your examples, there is an 'onFailed' handler, does it 'consume' the rejection or rejects the promises up the chain as well? In Simon Brunel's QtPromise implementation there is a 'tapFail' that can be injected into the promise chain to make an action on promise rejection, but the rejection will be passed further.I'm a bit worried though that you are haven't taken a look at JavaScript's promises. Asynchronous programming in JavaScript, C#, and Scala are kind of state of the art, so I think you should consider reviewing those approaches to take the best bits into Qt. Additionally, if someone who is familiar with those technologies starts with Qt, they would recognize similar patterns and techniques, so it is definitely worth looking at.
Thanks for the feedback.
Yes,
onFailed()
'consumes' the rejection, i.e. if you havefuture.then(func1).then(func2).onFailed(...).then(func3)
: iffunc1
fails with an exception,func2
is skipped and.onFailed()
is called, followed byfunc3
(if.onFailed()
doesn't throw).The intention was to improve the existing QFuture and provide the basic functionality to make it more useful. Considering that many C++ users are probably already familiar with std::future and std::promise, transitioning to QFuture/QPromise should require minimal effort. Of course, that doesn't mean that we won't continue improving the existing functionality. Feel free to submit your suggestions here.
@Sona have a look at this as suggested by Dmitriy. That simple QPromise should be included in Qt6. This is exactly was I talking about and a very high quality implementation too.
I've seen it already, it's indeed a great project, but it's up to the author to decide whether to contribute to Qt or not. We tried to provide our own implementation of a similar functionality, and we are open to suggestions to improve the functionality further.
Well, he tried to contribute, but the Qt people abandoned the discussion at some point: https://bugreports.qt.io/browse/QTBUG-80908 That's all fair of course, but implying that the author decided not to contribute this to Qt6 is a bit of a stretch..
Hey Flavio, you should try Simon Brunel's implementation of Promise/A+: https://github.com/simonbrunel/qtpromise I've used it in a couple of projects and I must say I'd prefer it over the implementation in Qt 6. Exactly for the reason you mentioned: it's easier and is based on a specification. So anybody who has ever used promises in JavaScript would find their way with Simon's QtPromise
Thanks, interesting. This is what I meant. But I think such a core feature has to be provided by Qt in order to be usable in the long term. Qt's API should make use of these simple promises. Then we can start using them in our own code. Alas they are over engineering it.
I am too lazy to look in the implementation but I wonder how do you ensure that continuation of the future is called in the same thread as the function was called. From the higher level point of view, there could be a race - function can exit before continuation is added. Is the thread somehow reserved until all futures leave scope?
No, the thread is not reserved. If continuation is attached after the parent has already finished, it will run in the thread that owns the parent future. I might consider adding another version of .then() which takes a specific QThread*as parameter, to give more control in such cases (which may be useful for other scenarios as well).
Then the doc should be updated, it says > When the asynchronous computation represented by this future finishes, function will be invoked in the same thread in which this future has been running which is not always true=) Maybe it makes sense to use Inherit launch policy for the Function overload [0]? It that case, if future was async, it will be (synchronously) re-scheduled in the pool?
[0] https://github.com/qt/qtbase/blob/dev/src/corelib/thread/qfuture.h#L350
You're right, I'll update the docs :)
In case of Inherit policy, if the parent has been using Async, the continuation will be launched in a new thread (QThreadPool may decide to re-use the parent's thread, but it's not guaranteed). Spawning a new thread for each continuation may not be what the user always wants, so I don't think making it default is a good idea.
Can the mentioned "custom thread pool" have a custom QThread::Priority too?
No, passing a QThread::Priority is not supported.
Looks similar to QtPromise project, nice one. Any plans to add support for context tracking, something like discussed here:
https://github.com/simonbrunel/qtpromise/issues/35
?
Thanks for the feedback. Ability to specify a context seems to be useful, created this.
It definitely would sound cool if coroutines didn't exist. But considering they will soon get full compiler support in Qt's supported compilers, likely soon after Qt 6 is released, I feel like these improvements will become obsolete some months after being released. Coroutines will make asynchronous code way more concise than any imaginable implementation of promises or futures.
I hope the improvements on futures and promises will not impede the efforts for first class Qt support on coroutines - because personally, that's what I will care about in the near future.