Important
本主题介绍 协程 和 co_await 的概念,我们建议您在 UI 和非 UI 应用程序中都使用它们。 为简单起见,本介绍性主题中的大多数代码示例都显示了Windows控制台应用程序(C++/WinRT)项目。 本主题后面的代码示例确实使用协同例程,但为方便起见,控制台应用程序示例还在退出前继续使用阻止 get 函数调用,以便应用程序在完成输出打印之前不会退出。 你不会从 UI 线程执行该操作(调用阻止 get 函数)。 相反,你将使用 co_await 语句。 在 UI 应用程序中使用的技术在主题 “高级并发”和“异步”主题中介绍。
本介绍性主题介绍了一些方法,你可以使用 C++/WinRT 创建和使用Windows 运行时异步对象。 阅读本主题后,另请参阅 高级并发和异步,尤其是有关将用于 UI 应用程序的技术。
异步操作和 Windows 运行时“Async”函数
任何具有完成时间可能超过 50 毫秒的Windows 运行时 API 都作为异步函数实现(名称以“Async”结尾)。 异步函数的实现在另一个线程上启动工作,并使用表示异步操作的对象立即返回。 异步操作完成后,返回的对象包含工作产生的任何值。 Windows::Foundation Windows 运行时命名空间包含四种类型的异步操作对象。
- IAsyncAction,
- IAsyncActionWithProgress<TProgress>,
- IAsyncOperation<TResult>,和
- IAsyncOperationWithProgress<TResult、TProgress>。
其中每个异步操作类型都投影到 winrt::Windows::Foundation C++/WinRT 命名空间中的相应类型。 C++/WinRT 还包含一个内部的 await 适配器结构体。 你不会直接使用它,但是,由于该结构,你可以编写一个 co_await 语句,以协作等待返回这些异步操作类型之一的任何函数的结果。 你也可以编写返回这些类型的自己的协程。
异步Windows函数的示例是 SyndicationClient::RetrieveFeedAsync,该函数返回 IAsyncOperationWithProgress TResult、 TProgress<> 类型的异步操作对象。
让我们来看看使用 C++/WinRT 调用这类 API 的一些方法——先看阻塞式,再看非阻塞式。 为了说明基本思路,我们将在接下来的几个代码示例中使用Windows控制台应用程序(C++/WinRT)项目。 高级 并发和异步中讨论了更适合 UI 应用程序的技术。
阻止调用线程
下面的代码示例从 RetrieveFeedAsync 接收一个异步操作对象,并对该对象调用 get,以阻塞调用线程,直到异步操作的结果可用。
如果要将此示例直接复制粘贴到Windows控制台应用程序(C++/WinRT)项目的主源代码文件中,请先在项目属性中设置“不使用预编译标头”。
// main.cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;
void ProcessFeed()
{
Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
SyndicationClient syndicationClient;
SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
// use syndicationFeed.
}
int main()
{
winrt::init_apartment();
ProcessFeed();
}
调用 get 可以让编码更方便,而且它非常适合控制台应用或后台线程等你可能出于某种原因不想使用协程的场景。 但它既不是并发的,也不是异步的,因此不适用于 UI 线程(如果你试图在 UI 线程上使用它,那么在未优化的构建版本中会触发断言)。 为了避免阻塞 OS 线程,使其无法执行其他有用的工作,我们需要采用另一种方法。
编写协同例程
C++/WinRT 将 C++ 协同例程集成到编程模型中,以提供一种自然的方式来协作等待结果。 可以通过编写协程来创建自己的 Windows 运行时 异步操作。 在下面的代码示例中,ProcessFeedAsync 是协程。
注释
get 函数存在于 C++/WinRT 投影类型 winrt::Windows::Foundation::IAsyncAction 上,因此可以从任何 C++/WinRT 项目中调用函数。 找不到作为 IAsyncAction 接口的成员列出的函数,因为 get 不是实际Windows 运行时类型 IAsyncAction 的应用程序二进制接口(ABI)图面的一部分。
// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;
void PrintFeed(SyndicationFeed const& syndicationFeed)
{
for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
{
std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
}
}
IAsyncAction ProcessFeedAsync()
{
Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
SyndicationClient syndicationClient;
SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
PrintFeed(syndicationFeed);
}
int main()
{
winrt::init_apartment();
auto processOp{ ProcessFeedAsync() };
// do other work while the feed is being printed.
processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}
协程是一种可以被挂起并恢复执行的函数。 在上面的 ProcessFeedAsync 协程中,当执行到 co_await 语句时,该协程会异步发起对 RetrieveFeedAsync 的调用,然后立即挂起自身,并将控制权返回给调用方(在上面的示例中,即 main)。
main 随后可以在检索并打印订阅源的同时继续工作。 该操作完成后(即 RetrieveFeedAsync 调用完成时),ProcessFeedAsync 协程会从下一条语句继续执行。
可以将协同例程聚合到其他协同例程中。 或者,你可以调用 get 来阻止并等待它完成(如果存在结果)。 也可以将其传递给支持Windows 运行时的另一种编程语言。
还可以使用委托来处理异步操作和/或运算的完成事件和/或进度事件。 有关详细信息和代码示例,请参阅 异步操作和操作的委托类型。
如上所示,在上面的代码示例中,我们在退出 main 之前继续使用阻止 get 函数调用。 但这只是使应用程序在完成打印输出之前不会退出。
异步返回 Windows 运行时 类型
在下一个示例中,我们封装了针对特定 URI 对 RetrieveFeedAsync 的调用,从而得到一个异步返回 SyndicationFeed 的 RetrieveBlogFeedAsync 函数。
// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;
void PrintFeed(SyndicationFeed const& syndicationFeed)
{
for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
{
std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
}
}
IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
SyndicationClient syndicationClient;
return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}
int main()
{
winrt::init_apartment();
auto feedOp{ RetrieveBlogFeedAsync() };
// do other work.
PrintFeed(feedOp.get());
}
在上面的示例中,RetrieveBlogFeedAsync 返回一个 IAsyncOperationWithProgress,它同时具有进度和返回值。 在 RetrieveBlogFeedAsync 运行并检索源的同时,我们还可以执行其他工作。 然后,我们对该异步操作对象调用 get 以阻塞并等待其完成,然后获取该操作的结果。
如果要异步返回 Windows 运行时 类型,那么应返回一个 IAsyncOperation<TResult> 或一个 IAsyncOperationWithProgress<TResult, TProgress>。 任何第一方或第三方运行时类都符合条件,或者任何可传入 Windows 运行时 函数或可从 Windows 运行时 函数传出的类型也都符合条件(例如 int 或 winrt::hstring)。 如果尝试将其中一种异步操作类型与非Windows 运行时类型一起使用,编译器将帮助你解决“T 必须为 WinRT 类型”错误。
如果协程没有至少一个 co_await 语句,那么要被视为协程,它必须至少有一个 co_return 或 co_yield 语句。 在某些情况下,协同例程可以返回一个值而不引入任何异步,因此无需阻止或切换上下文。 下面是一个通过缓存某个值来实现这一点的示例(在第二次及之后的调用时)。
winrt::hstring m_cache;
IAsyncOperation<winrt::hstring> ReadAsync()
{
if (m_cache.empty())
{
// Asynchronously download and cache the string.
}
co_return m_cache;
}
异步返回非 Windows Runtime 类型
如果你以异步方式返回的类型不是 Windows 运行时 类型,则应返回并行模式库 (PPL) concurrency::task。 建议使用 并发::task ,因为它可提供比 std::future 更好的性能(以及更好的兼容性)。
小窍门
如果包含 <pplawait.h>,则可以将 concurrency::task 用作协程类型。
// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;
concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
return concurrency::create_task([]
{
Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
SyndicationClient syndicationClient;
SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
});
}
int main()
{
winrt::init_apartment();
auto firstTitleOp{ RetrieveFirstTitleAsync() };
// Do other work here.
std::wcout << firstTitleOp.get() << std::endl;
}
参数传递
对于同步函数,默认情况下应使用 const& 参数。 这将避免复制的开销(这涉及到引用计数,这意味着相互锁的增量和递减)。
// Synchronous function.
void DoWork(Param const& value);
但是,如果向协程传递引用参数,就可能会出问题。
// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
// While it's ok to access value here...
co_await DoOtherWorkAsync(); // (this is the first suspension point)...
// ...accessing value here carries no guarantees of safety.
}
在协程中,在到达第一个挂起点之前,执行都是同步进行的;此时控制权会返回给调用方,而调用帧也会超出作用域。 等到协程恢复执行时,引用参数所引用的源值可能已经发生了各种变化。 从协程的角度来看,引用参数的生命周期无法控制。 因此,在上面的示例中,我们可以一直安全地访问 value,直到 co_await;但在这之后就不能再访问它了。 如果调用方析构了 value,那么之后再尝试在协程内访问它将导致内存损坏。 如果函数在恢复后尝试使用值,则也不能安全地将值传递给 DoOtherWorkAsync。
为了确保参数在挂起和恢复之后仍能安全使用,你的协程默认应采用按值传递,从而确保以值方式捕获这些参数,并避免生命周期问题。 如果你确信这样做是安全的,那么可以偏离该指导意见的情况将极为罕见。
// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&
按值传递要求参数移动或复制成本较低;这通常是智能指针的情况。
也可以认为(除非你想要移动值)通过常量值传递是很好的做法。 它不会对你要复制的源值产生任何影响,但这样能让意图更明确,而且如果你不小心修改了副本,也会有所帮助。
// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);
另请参阅 标准数组和向量,其中说明了如何将标准向量传递给异步被调用方。
如果无法更改协程的签名,但可以更改其实现,那么可以在第一个 co_await 之前先创建一个局部副本。
IASyncAction DoWorkAsync(Param const& value)
{
auto safe_value = value;
// It's ok to access both safe_value and value here.
co_await DoOtherWorkAsync();
// It's ok to access only safe_value here (not value).
}
如果 Param 复制代价很高,那么就在第一个 co_await 之前只提取你需要的部分。
IASyncAction DoWorkAsync(Param const& value)
{
auto safe_data = value.data;
// It's ok to access safe_data, value.data, and value here.
co_await DoOtherWorkAsync();
// It's ok to access only safe_data here (not value.data, nor value).
}
安全地访问类成员协同例程中的 此 指针
请参阅 C++/WinRT 中的强引用和弱引用。
重要 API
- concurrency::task 类
- IAsyncAction 接口
- IAsyncActionWithProgress TProgress<> 接口
- IAsyncOperation<TResult> 接口
- IAsyncOperationWithProgress<TResult, TProgress> 接口
- SyndicationClient::RetrieveFeedAsync 方法
- SyndicationFeed 类