QCoro: Qt C++ 20 协程库介绍
C++20 推出了协程的实现(coroutines)。虽然开发一个支持协程特性的类库还是要花很多功夫的,但是使用一个开发好的类库则是非常嗨。这也是C++ 委员会一贯的原则:
如果你是类库开发者,必须足够有耐心学习拗口的特性。但如果是类库使用者,则直接吃语法糖爽歪歪就行了
协程是一种线程内的快速执行序切换功能,比回调函数调用起来要简单。这篇文章介绍了协程的基本概念。
QCoro 是 Qt 的一个协程库,利用C++20的特性,可以显著简化以前用信号-槽回调才能完成的操作。代码可从github下载。
例子
QCoro库提供了一组工具来使用C++20与Qt的协同程序。下面的例子演示了下载一些网络数据的实现对比(传统信号槽和协程)。
以前使用Lambda槽回调时,为了生存周期,需要显式new Qt对象,并deleteLater。
传统信号槽:
void MyClass::fetchData() {
auto *nam = new QNetworkAccessManager(this);
auto *reply = nam->get(QUrl{QStringLiteral("https://.../api/fetch")});
QObject::connect(reply, &QNetworkReply::finished,
[reply, nam]() {
const auto data = reply->readAll();
doSomethingWithData(data);
reply->deleteLater();
nam->deleteLater();
});
}
现在只要稍微注意一下上下文的生存周期,就可以直接用栈上的局部变量调用IO操作,并保持事件循环。
协程:
QCoro::Task<> MyClass::fetchData() {
QNetworkReply nam;
auto *reply = co_await nam.get(QUrl{QStringLiteral("https://.../api/fetch")});
const auto data = reply->readAll();
reply->deleteLater();
doSomethingWithData(data);
}
这里的神奇之处在于co-await关键字,它将方法fetchData()变成了一个协程,并在网络请求运行时暂停了它的执行。当请求完成时,协程将从暂停的位置恢复并继续。注意!当co_await 暂停时,Qt事件循环照常运行!
官网的解释
下面的段落试图以简单的方式解释什么是协同程序以及co_await做什么。我不能保证这些都是事实上正确的。有关更详细(和正确)的信息,请参阅本文底部链接的文章。
简单地说,Coroutine类似于普通函数,只是它们可以在中间挂起(和恢复)。当协同程序被挂起时,执行返回到调用了协同程序的函数。如果该函数也是协同程序,并且正在等待(co_awaiting)当前协同程序完成,则它也将被挂起,执行将返回到调用该协同程序的函数,依此类推,直到到达作为实际函数(而不是协同程序)的函数。在常规Qt程序的情况下,这个“顶级”非协同程序函数将是Qt的事件循环——这意味着当从Qt事件循环调用协同程序时,当您的协同程序被挂起时,Qt事件环将继续运行,直到再次恢复协同程序。
在许多其他事情中,这允许您编写异步代码,就像它是同步的一样,而不会阻塞Qt事件循环,并使应用程序无响应。请参阅本文档中的不同示例。
现在让我们看看co_await关键字。这个关键字告诉编译器,这是协同程序希望挂起的点,直到等待的对象(awaitable)准备就绪。任何类型都可以是可等待的——要么是因为它直接实现C++协同程序机械所需的接口,要么是因为提供了一些外部工具(如该库)来将该类型包装为实现可等待接口的东西。
C++协同程序引入了两个额外的关键字-co_return和co_yield:
从应用程序程序员的角度来看,co_return的行为与return完全相同,只是您不能在协同程序中使用常规return。然而,在引擎盖下有一些主要的区别,这可能是为什么有一个用于从协同程序返回的特殊关键字的原因。
coyield允许协同程序在不实际返回的情况下生成结果。可以用于写入生成器。目前,这个库没有对co_yield的支持/使用,因此我在这里不再赘述。