I have been dreaming about using coroutines ever since I learnt the concept but I’ve never been lucky enough to be able to give them even a single try. ECMAScript standardized them just as I returned back to working on C++, and then C++ standardized them again recently in 2020, but only containing low-level building blocks, so it is still dependant on you or someone else to write a library that leverages them before you can really put them to good use. Such a library now exists, it is QCoro, and as you can imagine by the naming it supports the Qt APIs (and very well). So, let’s test the waters of the current state of coroutines with Qt. We will look at a beginners beginner’s introduction of what might be the most revolutionary feature at the language level since the addition of lambda functions in C++ 11.
Written by Alejandro Exojo
2021/12/23
What are coroutines?
Very simply speaking (we won’t use any heavy standard terminology) a coroutine is like a “regular” or “usual” function. However, the implementation can suspend the execution at any point while enabling it to be resumed later on whilst keeping the current state of the function without the developer having to do any manual work. There are a few ways in which this can be useful. We are going to focus on just one use: making asynchronous code look like if it were synchronous. We won’t cover generators at all (but you should know that this is another very interesting use of coroutines).
The kind of functions that we aim to improve with coroutines more or less all follow this structure:
ReturnType functionName(/* possibly parameters */)
{
// (1) Basic setup. Create objects with behavior (like QFile, QTimer, etc.)
// as usual, on the stack, and initialize them.
// (2) Slow operation. Use some function in the object that is gonna produce
// a result, but that is gonna make us wait in some manner.
// (3) Use the result. Just return the result of the operation, or do
// something ourselves with it before finishing.
}
The key is the second point: The slow operation. Typically we have a few different ways to deal with the issue:
- Just block the thread for the slow operation. Sometimes you can allow yourself (or the customer) to save development time and bugs for some niche feature that the user is going use once every blue moon. Very risky to use, as a small unexpected, one-second stutter in the UI today might end up messing everything up. Sometimes this can be mitigated by spawning a nested event loop (a practice that I strongly advise against due to terrible past experiences).
- Block a separate thread instead. You can use the global thread pool (
QThreadPool::globalInstance
) to launch the slow function where it’s not going to make anyone else wait. That’s great when you can “fire and forget”, but not so convenient when you need to return a value as part of the main logic.QtConcurrent::run
, which returns aQFuture
(which in turn can be used to get the result of the function which run on the thread), might be more useful. Or subclassQThread
to have the code insiderun()
to obtain the value, then hold the result for later in a member variable. Or any other pattern using many of the threads that Qt allows you to do. - Move the code to an object that deals with the wait asynchronously. We do this most of the time, for example, when using
QNetworkAccessManager
. We have to wait for the result of its operations to be ready, and we do it by connecting to a signal and obtaining the result once emitted. The problem is that now the logic is split into two functions for just one wait. Even if you use a lambda to have the code nicely grouped and organized, this doesn’t scale well if you have many points where you have to be asynchronous.
The first solution is arguably not a solution. The last two are the usual best practices. However, they are inconvenient: you no longer have a function returning a result. You call a function, you get the result somewhere else (in a signal parameter, via a getter in an object, etc.).
To make the problem more obvious, let’s look at this example (obviously inspired by The Hitchhiker’s Guide to the Galaxy, and taken from a presentation on QCoro by its very author):
int theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()
{
std::this_thread::sleep_for(std::chrono::years{7'500'000});
return 42;
}
We can call that function and get the result (eventually) in the same call. It’s annoyingly slow, but it’s also as simple as it can be. It doesn’t need any documentation.
An asynchronous API is much harder to explain:
class DeepThought : public QObject {
Q_OBJECT
public:
void calculateTheAnswer();
signals:
void answerObtained(int answer);
}
Sure, it is not rocket science, but I would not be surprised if sending such a class in a merge request would get me two or three comments asking about the constructor, Doxygen documentation, etc. You probably get the point. Oh, did you spot the silly mistake? I forgot the last semicolon. See? It’s just harder to write!
Coroutines versus The Pyramid of Doom
When we have lots of layers of asynchronous code where the result is processed in a callback, we end up having what informally is known as “The Pyramid of Doom”. Code starts to look like the following (taken from an old blog post on JavaScript and jQuery):
(function($) {
$(function(){
$("button").click(function(e) {
$.get("/test.json", function(data, textStatus, jqXHR) {
$(".list").each(function() {
$(this).click(function(e) {
setTimeout(function() {
alert("Hello World!");
}, 1000);
});
});
});
});
});
})(jQuery);
I’ve not bothered recreating this in C++ with Qt because it gets even more messy having to instantiate the objects that make the requests, thinking about the captures, etc., but hopefully you’ll see the point.
Our desire is to improve this. Code using coroutines should look like the following pseudocode:
log("Starting request in 10 seconds");
await timer(10s);
if (cancelled())
return;
reply = await networkRequest("https://www.example.com");
data1 = await reply.data();
data2 = await compress(data1);
This hopefully looks simpler to read and write, doesn’t it?
Coroutines support us in writing code like this. Each use of await
causes suspension of the execution of a function. Imagine that each line with the await
is equivalent to having a breakpoint in the debugger, and you could have the function stopped, with everything saved at that point, ready to be resumed when you decide to.
Coroutines are similar except there is no debugger, and the function actually returns to the caller and can be resumed when it makes sense. With the QCoro library, the control will be back to the event loop, and the resuming of the execution will happen when it’s ready. It’s similar to using a nested QEventLoop
or a constant call into QCoreApplication::processEvents()
. The coroutines approach is much cleaner, safer and doesn’t rely on bad habits.
Our first use of QCoro
QCoro is a library that provides support of the high-level features that we want on Qt types, to the world of vanilla C++ 20. In the 2020 specification of the language, only some low-level primitives are provided. Thankfully, there are 3rd party libraries to provide more convenient support for us mere mortals, and QCoro doesn’t need any patch on Qt’s source to support many asynchronous operations on Qt classes.
Mind you, the support of coroutines in compilers is a bit uneven, and the QCoro library is also somewhat young. Introduce coroutines with maximum care in your projects.
We are going to make a very simple example: Use the git client to retrieve the last version of Qt tagged in the qtbase repository, without cloning that repository. A quick web search will give us a simple way to do it with shell pipeline in one line. We are going to be extremely lazy, and just adapt the same to C++ using QProcess
. Since our goal is to practice coroutines, we are going to be silly and just feed the output of the git client to another shell program to filter it (we could do the filtering in C++ code, of course, but this will allow us to show better the usefulness of the feature).
First, this is how we are going to invoke git:
QStringList gitArguments()
{
return QStringList()
<< "-c"
<< "versionsort.suffix=-"
<< "ls-remote"
<< "--refs"
<< "--exit-code" // To exit with a failure if did not match
<< "--sort=version:refname"
<< "https://github.com/qt/qtbase"
<< "v*.*.*"; // Naming of tags in Qt's repositories.
}
QByteArray
output by newlines and taking the last. But as I said before, we are going to work like in this meme (not because the meme is true, but because it will make us use coroutines more).This is how we start launching the process, and how we work with it asynchronously:
QProcess process;
auto coroProcess = qCoro(process);
process.start("/usr/bin/git", gitArguments());
co_await coroProcess.waitForFinished();
We create a QProcess
as usual, but we also create a new variable via the qCoro
function. Many uses of the QCoro library don’t require this wrapper at all. Simply using the co_await
keyword on a Qt type (e.g. QNetworkReply
) does all the magic. The problem is that in the case of QProcess
there is more than one function that we want to be handled asynchronously, so we need the wrapper. You will see that we use the process
variable for regular QProcess
usage and the coroProcess
one for the coroutine’s suspension. The latter just refers to the former.
So, what is it that this first use of co_await
does? First, it makes the code where it is a coroutine instead of a regular function. Second, it keeps the state of the function saved and returns to the caller. The caller can retrieve the result by calling co_await
. We will see later more about calling this function. Let’s keep looking at how to implement it. This is how the code follows:
QByteArray output = process.readAllStandardOutput();
QByteArray errors = process.readAllStandardError();
if (!errors.isEmpty()) {
qDebug() << errors;
co_return QString();
}
Since we waited on the waitForFinished()
call, these lines of code will be executed only after the process has finished, of course. We just check for errors and return an empty string as a result of the version. More on the return type later, but note that we need to use co_return
instead of return
.
co_await coroProcess.start("/usr/bin/tail", QStringList() << "--lines=1");
co_await coroProcess.write(output);
process.closeWriteChannel();
bool allWritten = process.waitForBytesWritten();
qDebug() << "Now, all written?" << allWritten;
co_await coroProcess.waitForFinished();
output = process.readAllStandardOutput();
errors = process.readAllStandardError();
if (!errors.isEmpty()) {
qDebug() << errors;
co_return QString();
}
co_await
makes the wait on the event loop without blocking the thread. We handle the errors as we did before.
const auto lineParts = output.split('/');
if (lineParts.size() < 3)
co_return QString();
co_return lineParts.at(2);
Finally, we handle the final output, and this time we are not going to be as silly as before. We are using C++ to split the line instead of using yet another process. Now we can return the result, and again we use co_return
.
Now, where do we put that code? We said that the use of some keywords makes that a coroutine. Does this need some special syntax to make it a coroutine? Not much, as a good old function becomes a coroutine if it uses some of the new keywords that we’ve seen. However, we need to have a proper return type that satisfies some requirements. Thankfully, the QCoro::Task
template class is all that we need to use. We just pass as template argument the type that we want to return, which in our case is just a QString
. So the signature of the function would look like QCoro::Task lastQtRelease()
.
And how do we use it? Just call it, and use co_await
on it to wait for the result. This is an example on how to use it from a main()
function. We use a helper lambda (because lambdas can be coroutines, and it has to be one in this example) because the main function cannot be. We just call it via a 0 seconds timer, so it gets called from the event loop, and its execution goes back to the event loop when waiting. Like this:
int main(int argc, char** argv)
{
QCoreApplication application(argc, argv);
QTimer::singleShot(0, [&]() -> QCoro::Task<> {
QTextStream(stdout) << "** Result: " << co_await lastQtRelease();
application.quit();
});
application.exec();
}
Conclusions and caveats
This was just a very simple example to introduce you to coroutines. The feature is fairly new in C++, and not all compilers support it. I tested with GCC 10, and I got an ICE (internal compiler error; the compiler crashed) in the very first minutes of trying the feature. Clang has it marked as experimental and requires different headers and flags, which made Qt Creator a bit useless in the C support. I had to close it because the simple C file containing those lines made the fans of the laptop start spinning like crazy. Given that most tooling is based on Clang, this is a problem. MSVC should support coroutines as well, but I’ve not tried it.
The QCoro library is young, in version 0.3.0 at the moment of writing these lines. I think the new API in Qt will eventually make the above examples even nicer to use, as (if I’m not mistaken) the need for the wrapper has to do with the deduction of the return type. In many other situations, the code does use the same objects of the Qt API that you are used to, and just sprinkling these new keywords does all the magic (OK, and using QCoro::Task
as return type instead of T
). Like this example from the QCoro documentation:
QNetworkAccessManager networkAccessManager;
const QNetworkReply *reply = co_await networkAccessManager.get(url);
Coroutines are not necessarily making your code faster. There is some machinery required for handling the coroutine suspension and activation. And the variables that you put “on the stack” (without new
) are not really on the stack (because they need to be saved for resuming later, so under the hood, the whole frame for the coroutine is allocated with new
). But the convenience and clarity are massive if you need to chain slow operations one after the other.
I’m quite happy to have found the QCoro library and its excellent documentation, and I am quite thankful for the quick response from Daniel Vrátil (the author of the library). See his presentation at the last Akademy (either slides or video).