使用 C++/WinRT 执行并发和异步操作Concurrency and asynchronous operations with C++/WinRT

重要

本主题介绍协同例程和 co_await 的概念,我们建议你在 UI 应用程序和非 UI 应用程序中使用它们。This topic introduces the concepts of coroutines and co_await, which we recommend that you use in both your UI and in your non-UI applications. 为了简单起见,本介绍主题中的大多数代码示例演示了 Windows 控制台应用程序 (C++/WinRT) 项目。For simplicity, most of the code examples in this introductory topic show Windows Console Application (C++/WinRT) projects. 本主题中后面的代码示例使用协同例程,但为方便起见,控制台应用程序示例还会在退出前继续使用阻止性的 get 函数调用,这样应用程序就不会在显示其输出之前退出。The later code examples in this topic do use coroutines, but for convenience the console application examples also continue to use the blocking get function call just before exiting, so that the application doesn't exit before finishing printing its output. 不要通过 UI 线程这样做(调用阻止性的 get 函数),You won't do that (call the blocking get function) from a UI thread. 而应使用 co_await 语句。Instead, you'll use the co_await statement. 更高级的并发和异步主题介绍了将要在 UI 应用程序中使用的技术。The techniques that you'll use in your UI applications are described in the topic More advanced concurrency and asynchrony.

本简介性主题介绍了可通过 C++/WinRT 创建和使用 Windows 运行时异步对象的部分方式。This introductory topic shows some of the ways in which you can both create and consume Windows Runtime asynchronous objects with C++/WinRT. 阅读本主题后,如需其他技术,尤其是将要在 UI 应用程序中使用的技术,另请参阅更高级的并发和异步After reading this topic, especially for techniques you'll use in your UI applications, also see More advanced concurrency and asynchrony.

异步操作和 Windows 运行时“Async”函数Asynchronous operations and Windows Runtime "Async" functions

有可能需要超过 50 毫秒才能完成的任何 Windows 运行时 API 将实现为异步函数(具有一个以“Async”结尾的名称)。Any Windows Runtime API that has the potential to take more than 50 milliseconds to complete is implemented as an asynchronous function (with a name ending in "Async"). 异步函数的实现会启动另一线程上的工作,并且会立即返回表示异步操作的对象。The implementation of an asynchronous function initiates the work on another thread, and returns immediately with an object that represents the asynchronous operation. 在异步操作完成后,返回的对象会包含从该工作中生成的任何值。When the asynchronous operation completes, that returned object contains any value that resulted from the work. Windows::Foundation Windows 运行时命名空间包含四种类型的异步操作对象。The Windows::Foundation Windows Runtime namespace contains four types of asynchronous operation object.

每种异步操作类型都将投影到 winrt::Windows::Foundation C++/WinRT 命名空间中的相应类型。Each of these asynchronous operation types is projected into a corresponding type in the winrt::Windows::Foundation C++/WinRT namespace. C++/WinRT 还包含内部 await 适配器结构。C++/WinRT also contains an internal await adapter struct. 不要直接使用它,但借助该结构,可以编写 co_await 语句以协作等待返回其中一种异步操作类型的任何函数的结果。You don't use it directly but, thanks to that struct, you can write a co_await statement to cooperatively await the result of any function that returns one of these asynchronous operation types. 然后,可以自行创作返回这些类型的协同例程。And you can author your own coroutines that return these types.

异步 Windows 函数的示例是 SyndicationClient::RetrieveFeedAsync,其返回类型 IAsyncOperationWithProgress<TResult, TProgress> 的异步操作对象。An example of an asynchronous Windows function is SyndicationClient::RetrieveFeedAsync, which returns an asynchronous operation object of type IAsyncOperationWithProgress<TResult, TProgress>.

让我们来看一些阻塞和不阻塞使用 C++/WinRT 来调用类似 API 的方法。Let's look at some ways—first blocking, and then non-blocking—of using C++/WinRT to call an API such as that. 我们将在接下来的几个代码示例中使用 Windows 控制台应用程序 (C++/WinRT) 项目,只为说明基本的概念。Just for illustration of the basic ideas, we'll be using a Windows Console Application (C++/WinRT) project in the next few code examples. 更适用于 UI 应用程序的技术在更高级的并发和异步中讨论。Techniques that are more appropriate for a UI application are discussed in More advanced concurrency and asynchrony.

阻塞调用线程Block the calling thread

以下代码示例接收来自 RetrieveFeedAsync 的异步操作对象,并且在该对象上调用 get 以阻塞调用线程,直到异步操作的结果可用。The code example below receives an asynchronous operation object from RetrieveFeedAsync, and it calls get on that object to block the calling thread until the results of the asynchronous operation are available.

若要将此示例直接复制并粘贴到 Windows 控制台应用程序 (C++/WinRT) 项目的主源代码文件中,请先在项目属性中设置“不使用预编译的标头”。****If you want to copy-paste this example directly into the main source code file of a Windows Console Application (C++/WinRT) project, then first set Not Using Precompiled Headers in project properties.

// 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 可以方便编写代码,对于出于任何原因不想使用协同例程的控制台应用或后台线程来说,这是一种理想选择。Calling get makes for convenient coding, and it's ideal for console apps or background threads where you may not want to use a coroutine for whatever reason. 但这既不是并发也不是异步操作,因此不适合 UI 线程(如果试图在 UI 线程上使用它,会在未优化的版本中触发断言)。But it's not concurrent nor asynchronous, so it's not appropriate for a UI thread (and an assertion will fire in unoptimized builds if you attempt to use it on one). 为了避免占用 OS 线程执行其他有用的工作,我们需要另一种方法。To avoid holding up OS threads from doing other useful work, we need a different technique.

编写协同例程Write a coroutine

C++/WinRT 将 C++ 协同例程集成到编程模型中以提供协作等待结果的自然方式。C++/WinRT integrates C++ coroutines into the programming model to provide a natural way to cooperatively wait for a result. 可以通过编写协同例程来生成自己的 Windows 运行时异步操作。You can produce your own Windows Runtime asynchronous operation by writing a coroutine. 在以下代码示例中,ProcessFeedAsync 是协同例程。In the code example below, ProcessFeedAsync is the coroutine.

备注

get 函数位于 C++/WinRT 投影类型 winrt::Windows::Foundation::IAsyncAction 中,因此你可以从任意 C++/WinRT 项目内部调用该函数。The get function exists on the C++/WinRT projection type winrt::Windows::Foundation::IAsyncAction, so you can call the function from within any C++/WinRT project. 你将找不到列为 IAsyncAction 接口成员的函数,因为 get 不属于实际 Windows 运行时类型 IAsyncAction 的应用程序二进制接口 (ABI) 设计面。You will not find the function listed as a member of the IAsyncAction interface, because get is not part of the application binary interface (ABI) surface of the actual Windows Runtime type IAsyncAction.

// 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.
}

协同例程是可以暂停和恢复的函数。A coroutine is a function that can be suspended and resumed. 在上述 ProcessFeedAsync 协同例程中,当达到 co_await 语句时,该协同例程会异步启动 RetrieveFeedAsync 调用,然后立即暂停自身并将控件返回到调用方(上述示例中为 main)。In the ProcessFeedAsync coroutine above, when the co_await statement is reached, the coroutine asynchronously initiates the RetrieveFeedAsync call and then it immediately suspends itself and returns control back to the caller (which is main in the example above). 然后,main 可以继续执行工作,同时将检索并打印提要。main can then continue to do work while the feed is being retrieved and printed. 完成该操作(RetrieveFeedAsync 调用完成)后,ProcessFeedAsync 协同例程将在下一个语句中恢复。When that's done (when the RetrieveFeedAsync call completes), the ProcessFeedAsync coroutine resumes at the next statement.

可以将一个协同例程聚合到其他协同例程中。You can aggregate a coroutine into other coroutines. 或者,也可以调用 get 以阻塞和等待其完成(以及获得结果,如果有)。Or you can call get to block and wait for it to complete (and get the result if there is one). 或者,可以将其传递到支持 Windows 运行时的其他编程语言。Or you can pass it to another programming language that supports the Windows Runtime.

也可以通过使用委托来处理异步操作的已完成和/或正在进行中的事件。It's also possible to handle the completed and/or progress events of asynchronous actions and operations by using delegates. 有关详细信息和代码示例,请参阅异步操作的委托类型For details, and code examples, see Delegate types for asynchronous actions and operations.

正如你所看到的,在上面的代码示例中,我们在退出 main 之前继续使用阻止性的 get 函数调用。As you can see, in the code example above, we continue to use the blocking get function call just before exiting main. 但是,这只是为了让应用程序不会在显示其输出之前退出。But that's only so that the application doesn't exit before finishing printing its output.

异步返回 Windows 运行时类型Asynchronously return a Windows Runtime type

在下一个示例中,我们将针对特定 URI 封装对 RetrieveFeedAsync 的调用,以为我们提供异步返回 SyndicationFeedRetrieveBlogFeedAsync 函数。In this next example we wrap a call to RetrieveFeedAsync, for a specific URI, to give us a RetrieveBlogFeedAsync function that asynchronously returns a SyndicationFeed.

// 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,其具有进度值和返回值。In the example above, RetrieveBlogFeedAsync returns an IAsyncOperationWithProgress, which has both progress and a return value. 我们可以在 RetrieveBlogFeedAsync 执行其操作并检索提要的同时进行其他工作。We can do other work while RetrieveBlogFeedAsync is doing its thing and retrieving the feed. 然后,在该异步操作对象上调用 get,以阻塞、等待其完成,然后获取该操作的结果。Then, we call get on that asynchronous operation object to block, wait for it to complete, and then obtain the results of the operation.

如果要异步返回 Windows 运行时类型,则应返回 IAsyncOperation<TResult>IAsyncOperationWithProgress<TResult, TProgress>If you're asynchronously returning a Windows Runtime type, then you should return an IAsyncOperation<TResult> or an IAsyncOperationWithProgress<TResult, TProgress>. 任何第一方或第三方运行时类或可以传入/传出 Windows 运行时函数的任何类型(例如 intwinrt::hstring)都符合条件。Any first- or third-party runtime class qualifies, or any type that can be passed to or from a Windows Runtime function (for example, int, or winrt::hstring). 如果尝试对非 Windows 运行时类型使用其中一种异步操作类型,编译器可帮助你处理“T 必须为 WinRT 类型”错误。The compiler will help you with a "T must be WinRT type" error if you try to use one of these asynchronous operation types with a non-Windows Runtime type.

如果协同例程没有至少一条 co_await 语句,则为了符合成为协同例程的资格,它必须至少有一条 co_return 或一条 co_yield 语句。If a coroutine doesn't have at least one co_await statement then, in order to qualify as a coroutine, it must have at least one co_return or one co_yield statement. 在某些情况下,协同例程可以返回值而不引入任何异步,因此不阻塞也不切换上下文。There will be cases where your coroutine can return a value without introducing any asynchrony, and therefore without blocking nor switching context. 下面是一个通过缓存值来实现上述功能(第二次及后续调用时)的示例。Here's an example that does that (the second and subsequent times it's called) by caching a value.

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

异步返回非 Windows 运行时类型Asynchronously return a non-Windows-Runtime type

如果要异步返回非 Windows 运行时类型的类型,则应返回并行模式库 (PPL) concurrency::task。**If you're asynchronously returning a type that's not a Windows Runtime type, then you should return a Parallel Patterns Library (PPL) concurrency::task. 建议使用 concurrency::task,因为它将提供比 std::future 更好的性能(以及更好的兼容性)。We recommend concurrency::task because it gives you better performance (and better compatibility going forward) than std::future does.

提示

如果包含 <pplawait.h>,则可以使用 concurrency::task 作为协同例程类型。If you include <pplawait.h>, then you can use concurrency::task as a coroutine type.

// 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;
}

参数传递Parameter-passing

对于同步函数,默认情况下应该使用 const& 参数。For synchronous functions, you should use const& parameters by default. 这将避免复制开销(涉及引用计数,意味着互锁的增加和减少)。That will avoid the overhead of copies (which involve reference counting, and that means interlocked increments and decrements).

// Synchronous function.
void DoWork(Param const& value);

但如果向协同例程传递引用参数,可能会遇到问题。But you can run into problems if you pass a reference parameter to a coroutine.

// 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.
}

在协同程序中,在第一个暂停点之前,执行是同步的;到达第一个暂停点时,控制返回到调用方,调用帧超出范围。In a coroutine, execution is synchronous up until the first suspension point, where control is returned to the caller and the calling frame goes out of scope. 在协同例程恢复时,引用参数引用的源值可能已发生更改。By the time the coroutine resumes, anything might have happened to the source value that a reference parameter references. 从协同例程的角度来看,引用参数具有不受控制的生命周期。From the coroutine's perspective, a reference parameter has uncontrolled lifetime. 因此,在上面的示例中,在 co_await 之前,我们可以安全地访问 value,但之后就无法保证安全了。So, in the example above, we're safe to access value up until the co_await, but not after it. 如果调用方销毁了 value,则尝试在协同例程中访问它会导致内存损坏。In the event that value is destructed by the caller, attempting to access it inside the coroutine after that results in a memory corruption. 如果 DoOtherWorkAsync 函数有可能暂停并在恢复后尝试使用 value,我们也无法安全地将 value 传递给 DoOtherWorkAsync。Nor can we safely pass value to DoOtherWorkAsync if there's any risk that that function will in turn suspend and then try to use value after it resumes.

为了能够在暂停和恢复后安全地使用参数,默认情况下,协同例程应使用按值传递,以确保按值进行捕获并避免生命周期问题。To make parameters safe to use after suspending and resuming, your coroutines should use pass-by-value by default to ensure that they capture by value, and avoid lifetime issues. 确信不遵从该指引也能安全进行操作的情况是很少见的。Cases when you can deviate from that guidance because you're certain that it's safe to do so are going to be rare.

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

按值传递要求参数的移动或复制开销不高,智能指针通常就是这样的。Passing by value requires that the argument be inexpensive to move or copy; and that's typically the case for a smart pointer.

传递 const 值是否是一个好的做法也还存在争议(除非你想移动值)。It's also arguable that (unless you want to move the value) passing by const value is good practice. 它不会对要复制的源值产生任何影响,但有助于表明意图,并避免你无意间修改副本。It won't have any effect on the source value from which you're making a copy, but it makes the intent clear, and helps if you inadvertently modify the copy.

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

另请参阅标准数组和向量,其中介绍了如何将标准向量传递到异步被调用方。Also see Standard arrays and vectors, which deals with how to pass a standard vector into an asynchronous callee.

如果不能更改协同例程的签名,但是能够更改实现,则可在首次执行 co_await 之前进行本地复制。If you can't change your coroutine's signature, but you can change the implementation, then you can make a local copy before the first 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 之前只提取所需的片段。If Param is expensive to copy, then extract just the pieces you need before the first 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).
}

在类成员协同例程中安全访问 this 指针Safely accessing the this pointer in a class-member coroutine

请参阅 C++/WinRT 中的强引用和弱引用See Strong and weak references in C++/WinRT.

重要的 APIImportant APIs