C++/WinRT 的更高级并发和异步More advanced concurrency and asynchrony with C++/WinRT

本主题介绍使用 C++/WinRT 的具有并发性和异步性的更高级方案。This topic describes more advanced scenarios with concurrency and asynchrony in C++/WinRT.

有关此主题的简介,请首先阅读并发和异步运算For an introduction to this subject, first read Concurrency and asynchronous operations.

将工作卸载到 Windows 线程池Offloading work onto the Windows thread pool

协同例程与任何其他函数的类似之处在于,调用方将会阻塞到某个函数向其返回了执行为止。A coroutine is a function like any other in that a caller is blocked until a function returns execution to it. 另外,协同例程返回的第一个机会是第一个 co_awaitco_returnco_yieldAnd, the first opportunity for a coroutine to return is the first co_await, co_return, or co_yield.

因此,在协同例程中执行受计算限制的工作之前,需要将执行返回给调用方(换句话说,引入暂停点),使调用方不被阻塞。So, before you do compute-bound work in a coroutine, you need to return execution to the caller (in other words, introduce a suspension point) so that the caller isn't blocked. 如果还没有对其他某个操作运行 co_await 来做到这一点,则可以对 winrt::resume_background 函数运行 co_awaitIf you're not already doing that by co_await-ing some other operation, then you can co_await the winrt::resume_background function. 这会将控制权返回给调用方,然后立即在某个线程池线程上恢复执行。That returns control to the caller, and then immediately resumes execution on a thread pool thread.

实现中使用的线程池是底层 Windows 线程池,因此具有极高的效率。The thread pool being used in the implementation is the low-level Windows thread pool, so it's optimially efficient.

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

编程时仔细考虑线程相关性Programming with thread affinity in mind

该方案继续对上一个方案进行扩展。This scenario expands on the previous one. 你已将一些工作卸载到线程池,但希望在用户界面 (UI) 中显示进度。You offload some work onto the thread pool, but then you want to display progress in the user interface (UI).

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

上述代码抛出一个 winrt::hresult_wrong_thread 异常,因为必须从创建 TextBlock 的线程(即 UI 线程)更新 TextBlock。The code above throws a winrt::hresult_wrong_thread exception, because a TextBlock must be updated from the thread that created it, which is the UI thread. 一种解决方案是捕获最初调用协同例程的线程上下文。One solution is to capture the thread context within which our coroutine was originally called. 为此,请实例化 winrt::apartment_context 对象,执行后台工作,然后对 apartment_context 运行 co_await 以切回到调用上下文。To do that, instantiate a winrt::apartment_context object, do background work, and then co_await the apartment_context to switch back to the calling context.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

只要上面的协同例程是从创建 TextBlock 的 UI 线程调用的,这种方法是可行的。As long as the coroutine above is called from the UI thread that created the TextBlock, then this technique works. 在应用中,有很多时候都是可以保证这一点的。There will be many cases in your app where you're certain of that.

若要通过某种更通用的解决方案来更新 UI(包括不确定调用线程的情况)可以对 winrt::resume_foreground 函数运行 co_await,以切换到特定的前台线程。For a more general solution to updating UI, which covers cases where you're uncertain about the calling thread, you can co_await the winrt::resume_foreground function to switch to a specific foreground thread. 在以下代码示例中,我们通过传递与 TextBlock 关联的调度程序对象(通过访问其 Dispatcher 属性)来指定前台线程。In the code example below, we specify the foreground thread by passing the dispatcher object associated with the TextBlock (by accessing its Dispatcher property). winrt::resume_foreground 实现对该调度程序对象调用 CoreDispatcher.RunAsync,以执行协同例程中该调度程序对象之后的工作。The implementation of winrt::resume_foreground calls CoreDispatcher.RunAsync on that dispatcher object to execute the work that comes after it in the coroutine.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.Dispatcher());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

winrt::resume_foreground 函数采用可选的优先级参数。The winrt::resume_foreground function takes an optional priority parameter. 如果使用该参数,则可以使用上面所示的模式。If you're using that parameter, then the pattern shown above is appropriate. 如果不使用,则可以选择将 co_await winrt::resume_foreground(someDispatcherObject); 简化为 co_await someDispatcherObject;If not, then you can choose to simplify co_await winrt::resume_foreground(someDispatcherObject); into just co_await someDispatcherObject;.

协同例程中的执行上下文、恢复和切换Execution contexts, resuming, and switching in a coroutine

概括地说,在协同例程中某个暂停点之后,原始执行线程可能会消失,而恢复可能会在任何线程上发生(换而言之,任何线程都可以针对异步操作调用 Completed 方法)。Broadly speaking, after a suspension point in a coroutine, the original thread of execution may go away and resumption may occur on any thread (in other words, any thread may call the Completed method for the async operation).

但是,如果对四个 Windows 运行时异步操作类型 (IAsyncXxx) 中的任何一个运行 co_await,则 C++/WinRT 会在运行 co_await 时捕获调用上下文。But if you co_await any of the four Windows Runtime asynchronous operation types (IAsyncXxx), then C++/WinRT captures the calling context at the point you co_await. 另外,它可以当延续操作恢复时,你仍处于该上下文中。And it ensures that you're still on that context when the continuation resumes. 为此,C++/WinRT 会检查你是否已进入调用上下文,如果没有,则切换到该上下文。C++/WinRT does this by checking whether you're already on the calling context and, if not, switching to it. 如果在运行 co_await 之前你处于单线程单元 (STA) 线程中,则运行之后你仍处于相同的线程中;如果在运行 co_await 之前你处于多线程单元 (MTA) 线程中,则运行之后你将处于不同的线程中。If you were on a single-threaded apartment (STA) thread before co_await, then you'll be on the same one afterward; if you were on a multi-threaded apartment (MTA) thread before co_await, then you'll be on one afterward.

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

可以依赖此行为的原因在于,C++/WinRT 提供相应的代码,使这些 Windows 运行时异步操作类型能够适应 C++ 协同例程语言支持(这些代码片段称为等待适配器)。The reason you can rely on this behavior is because C++/WinRT provides code to adapt those Windows Runtime asynchronous operation types to the C++ coroutine language support (these pieces of code are called wait adapters). C++/WinRT 中剩余的可等待类型只是一些线程池包装器和/或帮助器;因此它们会在线程池中完成。The remaining awaitable types in C++/WinRT are simply thread pool wrappers and/or helpers; so they complete on the thread pool.

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

如果对其他某个类型运行 co_await— 即使是在 C++/WinRT 协同例程实现中 — 则另一个库会提供适配器,你需要了解这些适配器在恢复和上下文方面的作用。If you co_await some other type—even within a C++/WinRT coroutine implementation—then another library provides the adapters, and you'll need to understand what those adapters do in terms of resumption and contexts.

为了尽量减少上下文切换次数,可以使用本主题所述的某些方法。To keep context switches down to a minimum, you can use some of the techniques that we've already seen in this topic. 让我们看看该操作的几个图示。Let's see some illustrations of doing that. 以下伪代码示例演示了一个事件处理程序的大纲。该处理程序调用 Windows 运行时 API 来加载图像,切换到后台线程来处理该图像,然后返回到 UI 线程以在 UI 中显示该图像。In this next pseudo-code example, we show the outline of an event handler that calls a Windows Runtime API to load an image, drops onto a background thread to process that image, and then returns to the UI thread to display the image in the UI.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->Dispatcher());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

在此方案中,调用 StorageFile::OpenAsync 会使效率略微下降。For this scenario, there's a little bit of ineffiency around the call to StorageFile::OpenAsync. 恢复时有必要将上下文切换到后台线程(这样,处理程序便可以将执行返回给调用方),然后 C++/WinRT 会还原 UI 线程上下文。There's a necessary context switch to a background thread (so that the handler can return execution to the caller), on resumption after which C++/WinRT restores the UI thread context. 但是,在此情况下,在我们即将更新 UI 之前,没有必要处于 UI 线程中。But, in this case, it's not necessary to be on the UI thread until we're about to update the UI. 在调用 winrt::resume_background 之前调用的 Windows 运行时 API 越多,发生的不必要往返上下文切换也越多。The more Windows Runtime APIs we call before our call to winrt::resume_background, the more unnecessary back-and-forth context switches we incur. 解决方法是在此之前不要调用任何 Windows 运行时 API。The solution is not to call any Windows Runtime APIs before then. 将所有此类调用移到 winrt::resume_background 的后面。Move them all after the winrt::resume_background.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->Dispatcher());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

如果需要提前执行某些调用,可以编写自己的 await 适配器。If you want to do something more advanced, then you could write your own await adapters. 例如,若要运行 co_await 以便在完成异步操作所在的同一线程上进行恢复(因此不会发生上下文切换),可以先编写如下所示的 await 适配器。For example, if you want a co_await to resume on the same thread that the async action completes on (so, there's no context switch), then you could begin by writing await adapters similar to the ones shown below.

备注

以下代码示例仅用于培训目的,可帮助你开始了解 await 适配器的工作原理。The code example below is provided for educational purposes only; it's to get you started understanding how await adapters work. 若要在自己的基代码中使用此方法,我们建议你开发并测试自己的 await 适配器结构。If you want to use this technique in your own codebase, then we recommend that you develop and test your own await adapter struct(s). 例如,可以编写 complete_on_anycomplete_on_currentcomplete_on(dispatcher)For example, you could write complete_on_any, complete_on_current, and complete_on(dispatcher). 另请考虑将这些结构设置为使用 IAsyncXxx 类型作为模板参数的模板。Also consider making them templates that take the IAsyncXxx type as a template parameter.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

若要了解如何使用 no_switch await 适配器,首先需要知道,当 C++ 编译器遇到 co_await 表达式时,它会查找名为 await_readyawait_suspendawait_resume 的函数。To understand how to use the no_switch await adapters, you'll first need to know that when the C++ compiler encounters a co_await expression it looks for functions called await_ready, await_suspend, and await_resume. C++/WinRT 库提供了这些函数,使你在默认情况下能够获得合理行为,如下所示。The C++/WinRT library provides those functions so that you get reasonable behavior by default, like this.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

若要使用 no_switch await 适配器,只需将该 co_await 表达式的类型从 IAsyncXxx 更改为 no_switch,如下所示。To use the no_switch await adapters, just change the type of that co_await expression from IAsyncXxx to no_switch, like this.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

然后,C++ 编译器不会查找与 IAsyncXxx 匹配的三个 await_xxx 函数,而是查找与 no_switch 匹配的函数。Then, instead of looking for the three await_xxx functions that match IAsyncXxx, the C++ compiler looks for functions that match no_switch.

深入了解 winrt::resume_foregroundA deeper dive into winrt::resume_foreground

C++/WinRT 2.0 开始,winrt::resume_foreground 函数会暂停,即使是从调度程序线程来调用它(在以前的版本中,它可能会在某些情况下引入死锁,因为它暂停的前提是尚未位于调度程序线程上)。As of C++/WinRT 2.0, the winrt::resume_foreground function suspends even if it's called from the dispatcher thread (in previous versions, it could introduce deadlocks in some scenarios because it only suspended if not already on the dispatcher thread).

当前的行为意味着,你可以依赖于堆栈展开和进行的重新排队;这对于系统稳定很重要,尤其是在低级别系统代码中。The current behavior means that you can rely on stack unwinding and re-queuing taking place; and that's important for system stability, especially in low-level systems code. 在上面的编程时仔细考虑线程相关性部分列出的最后的代码演示了如何在后台线程上执行某些复杂的计算,然后切换到相应的 UI 线程,以便更新用户界面 (UI)。The last code listing in the section Programming with thread affinity in mind, above, illustrates performing some complex calculation on a background thread, and then switching to the appropriate UI thread in order to update the user interface (UI).

下面是 winrt::resume_foreground 的具体代码。Here's how winrt::resume_foreground looks internally.

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

这种当前行为与过去行为的对比类似于 Win32 应用程序开发过程中出现的 PostMessageSendMessage 之间的差异。This current, versus previous, behavior is analogous to the difference between PostMessage and SendMessage in Win32 application development. PostMessage 先将工作排队,然后在不等待工作完成的情况下展开堆栈。PostMessage queues the work and then unwinds the stack without waiting for the work to complete. 堆栈展开有时候很重要。The stack-unwinding can be essential.

winrt::resume_foreground 函数一开始也只支持在 Windows 10 之前引入的 CoreDispatcher(绑定到 CoreWindow)。The winrt::resume_foreground function also initially only supported the CoreDispatcher (tied to a CoreWindow), which was introduced prior to Windows 10. 自那以后,我们引入了更灵活高效的调度程序:DispatcherQueueWe've since introduced a more flexible and efficient dispatcher: the DispatcherQueue. 你可以创建适合自己使用的 DispatcherQueueYou can create a DispatcherQueue for your own purposes. 让我们考虑一下这个简单的控制台应用程序。Consider this simple console application.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

上面的示例在专用线程上创建一个队列(包含在控制器中),然后将控制器传递给协同程序。The example above creates a queue (contained within a controller) on a private thread, and then passes the controller to the coroutine. 协同程序可以使用队列在专用线程上等待(先暂停再继续)。The coroutine can use the queue to await (suspend and resume) on the private thread. DispatcherQueue 的另一常见用法是在传统桌面或 Win32 应用的当前 UI 线程上创建一个队列。Another common use of DispatcherQueue is to create a queue on the current UI thread for a traditional desktop or Win32 app.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

上面的代码演示了如何调用 Win32 函数并将其纳入 C++/WinRT 项目中,方法是:直接调用 Win32 样式的 CreateDispatcherQueueController 函数来创建控制器,然后将生成的队列控制器的所有权以 WinRT 对象形式移交给调用方。This illustrates how you can call and incorporate Win32 functions into your C++/WinRT projects, by simply calling the Win32-style CreateDispatcherQueueController function to create the controller, and then transfer ownership of the resulting queue controller to the caller as a WinRT object. 这也正是你能够在现有的 Petzold 样式 Win32 桌面应用程序上为高效无缝排队提供支持的方式。This is also precisely how you can support efficient and seamless queuing on your existing Petzold-style Win32 desktop application.

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

上面这个简单的 main 函数一开始就创建一个窗口。Above, the simple main function begins by creating a window. 你可以想象一下,这样会注册一个 window 类,然后调用 CreateWindow 来创建顶级桌面窗口。You can imagine that this registers a window class, and calls CreateWindow to create the top-level desktop window. 然后调用 CreateDispatcherQueueController 函数来创建队列控制器,再通过该控制器拥有的调度程序队列调用某个协同程序。CreateDispatcherQueueController function is then called to create the queue controller before calling some coroutine with the dispatcher queue owned by this controller. 然后进入传统的消息泵,在其中的此线程上自然而然地恢复协同程序。A traditional message pump is then entered where resumption of the coroutine naturally occurs on this thread. 然后,你可以回到协同程序所在的位置,在应用程序中完成异步的或基于消息的工作流。Having done that, you can return to the elegant world of coroutines for your async or message-based workflow within your application.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

调用 winrt::resume_foreground 时会始终先排队,然后展开堆栈。The call to winrt::resume_foreground will always queue, and then unwind the stack. 也可选择设置恢复优先级。You can also optionally set the resumption priority.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

或者,使用默认的排队顺序。Or, using the default queuing order.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

或者,检测队列关闭情况并对其进行适当处理,如以下示例所示。Or, in this case detecting queue shutdown, and handling it gracefully.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

co_await 表达式返回 true,表明会在调度程序线程上进行恢复。The co_await expression returns true, indicating that resumption will occur on the dispatcher thread. 换而言之,该排队操作是成功的。In other words, that queuing was successful. 与之相反的是返回 false,这表明执行仍保留在调用线程上,因为队列的控制器关闭,再也不能处理队列请求。Conversely, it returns false to indicate that execution remains on the calling thread because the queue's controller is shutting down and is no longer serving queue requests.

因此,在将 C++/WinRT 与协同程序配合使用的情况下,尤其是在进行一些传统的 Petzold 样式桌面应用程序开发的情况下,你拥有很大的控制权限。So, you have a great deal of power at your fingertips when you combine C++/WinRT with coroutines; and especially when doing some old-school Petzold-style desktop application development.

取消异步操作和取消回调Canceling an asynchronous operation, and cancellation callbacks

使用 Windows 运行时的异步编程功能可以取消正在进行的异步操作或运算。The Windows Runtime's features for asynchronous programming allow you to cancel an in-flight asynchronous action or operation. 以下示例调用 StorageFolder::GetFilesAsync 来检索可能较大的文件集合,并将生成的异步操作对象存储在数据成员中。Here's an example that calls StorageFolder::GetFilesAsync to retrieve a potentially large collection of files, and it stores the resulting asynchronous operation object in a data member. 用户可以选择取消该操作。The user has the option to cancel the operation.

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Windows::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

让我们通过一个简单的示例开始了解取消的实现端。For the implementation side of cancellation, let's begin with a simple example.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancellationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancellationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancellation{ ImplicitCancellationAsync() };
    co_await 3s;
    implicit_cancellation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

如果运行上述示例,则你会看到,ImplicitCancellationAsync 在三秒钟内每秒输出一条消息,然后,它会由于执行了取消操作而自动终止。If you run the example above, then you'll see ImplicitCancellationAsync print one message per second for three seconds, after which time it automatically terminates as a result of being canceled. 之所以此行为是可行的,是因为在遇到 co_await 表达式时,协同例程会检查它是否已取消。This works because, on encountering a co_await expression, a coroutine checks whether it has been cancelled. 如果已取消,则它会短路掉,否则它会像正常情况下一样暂停。If it has, then it short-circuits out; and if it hasn't, then it suspends as normal.

当然,取消也可以在协同例程暂停时发生。Cancellation can, of course, happen while the coroutine is suspended. 仅当协同例程恢复或者遇到另一个 co_await 时,它才会检查取消状态。Only when the coroutine resumes, or hits another co_await, will it check for cancellation. 问题在于,在响应取消时可能会出现过于粗糙粒度的延迟。The issue is one of potentially too-coarse-grained latency in responding to cancellation.

因此,另一种做法是从协同例程内部显式轮询取消。So, another option is to explicitly poll for cancellation from within your coroutine. 使用以下列表中的代码更新上述示例。Update the example above with the code in the listing below. 在此新示例中,ExplicitCancellationAsync 检索 winrt::get_cancellation_token 函数返回的对象,并使用该对象定期检查是否已取消协同例程。In this new example, ExplicitCancellationAsync retrieves the object returned by the winrt::get_cancellation_token function, and uses it to periodically check whether the coroutine has been canceled. 只要尚未取消,协同例程就会无限循环;一旦取消,循环和函数就会正常退出。As long as it's not canceled, the coroutine loops indefinitely; once it is canceled, the loop and the function exit normally. 结果与前一个示例相同,不过,在此示例中,退出是显式发生的并且受控。The outcome is the same as the previous example, but here exiting happens explicitly, and under control.

IAsyncAction ExplicitCancellationAsync()
{
    auto cancellation_token{ co_await winrt::get_cancellation_token() };

    while (!cancellation_token())
    {
        std::cout << "ExplicitCancellationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancellation{ ExplicitCancellationAsync() };
    co_await 3s;
    explicit_cancellation.Cancel();
}
...

等待 winrt::get_cancellation_token 时会使用协同例程代表你生成的 IAsyncAction 信息检索取消标记。Waiting on winrt::get_cancellation_token retrieves a cancellation token with knowledge of the IAsyncAction that the coroutine is producing on your behalf. 可以针对该标记使用函数调用运算符以查询取消状态 — 实质上是轮询取消。You can use the function call operator on that token to query the cancellation state—essentially polling for cancellation. 如果执行某项计算资源受限的操作,或循环访问大型集合,则这是一种合理的方法。If you're performing some compute-bound operation, or iterating through a large collection, then this is a reasonable technique.

注册取消回调Register a cancellation callback

Windows 运行时的取消不会自动流向其他异步对象。The Windows Runtime's cancellation doesn't automatically flow to other asynchronous objects. 但是,可以注册取消回调 — 这是 Windows SDK 版本 10.0.17763.0(Windows 10 版本 1809)中引入的功能。But—introduced in version 10.0.17763.0 (Windows 10, version 1809) of the Windows SDK—you can register a cancellation callback. 这是一种先发性的挂钩,可据此传播取消,以及与现有的并发库集成。This is a pre-emptive hook by which cancellation can be propagated, and makes it possible to integrate with existing concurrency libraries.

在以下代码示例中,NestedCoroutineAsync 将执行工作,但其中不包含特殊的取消逻辑。In this next code example, NestedCoroutineAsync does the work, but it has no special cancellation logic in it. CancellationPropagatorAsync 实质上是协同例程中的一个包装器;该包装器提前转发取消。CancellationPropagatorAsync is essentially a wrapper on the nested coroutine; the wrapper forwards cancellation pre-emptively.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancellationPropagatorAsync()
{
    auto cancellation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancellation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancellation_propagator{ CancellationPropagatorAsync() };
    co_await 3s;
    cancellation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancellationPropagatorAsync 为自身的取消回调注册一个 lambda 函数,然后等待(此时会暂停)嵌套的工作完成。CancellationPropagatorAsync registers a lambda function for its own cancellation callback, and then it awaits (it suspends) until the nested work completes. 如果 CancellationPropagatorAsync 已取消,则会将取消传播到嵌套的协同例程。When or if CancellationPropagatorAsync is canceled, it propagates the cancellation to the nested coroutine. 无需轮询取消;取消不会无限期阻塞。There's no need to poll for cancellation; nor is cancellation blocked indefinitely. 此机制足够灵活,使用它可以与完全不了解 C++/WinRT 的协同例程或并发库互操作。This mechanism is flexible enough for you to use it to interop with a coroutine or concurrency library that knows nothing of C++/WinRT.

报告进度Reporting progress

如果协同例程返回 IAsyncActionWithProgressIAsyncOperationWithProgress,则你可以检索 winrt::get_progress_token 函数返回的对象,并使用该对象将进度报告给进度处理程序。If your coroutine returns either IAsyncActionWithProgress, or IAsyncOperationWithProgress, then you can retrieve the object returned by the winrt::get_progress_token function, and use it to report progress back to a progress handler. 下面是代码示例。Here's a code example.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress(1.0);
    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& /* sender */, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

备注

对一个异步操作或运算实现多个完成处理程序是错误的做法 。It's not correct to implement more than one completion handler for an asynchronous action or operation. 可对其已完成的事件使用单个委托,或者可对其运行 co_awaitYou can have either a single delegate for its completed event, or you can co_await it. 如果同时采用这两种方法,则第二种方法会失败。If you have both, then the second will fail. 以下两种完成处理程序都是适当的;但不能同时对同一个异步对象使用两者。Either one of the following two kinds of completion handlers is appropriate; not both for the same async object.

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

有关完成处理程序的详细信息,请参阅异步操作和运算的委托类型For more info about completion handlers, see Delegate types for asynchronous actions and operations.

发后不理Fire and forget

有时,某个任务可与其他工作并发执行,你无需等待该任务完成(因为没有其他工作依赖于它),也无需该任务返回值。Sometimes, you have a task that can be done concurrently with other work, and you don't need to wait for that task to complete (no other work depends on it), nor do you need it to return a value. 在这种情况下,可以激发该任务,然后忘记它。In that case, you can fire off the task and forget it. 为此,可以编写返回类型为 winrt::fire_and_forget(而不是某个 Windows 运行时异步操作类型,或 concurrency:: task)的协同例程。You can do that by writing a coroutine whose return type is winrt::fire_and_forget (instead of one of the Windows Runtime asynchronous operation types, or concurrency::task).

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

winrt::fire_and_forget 也可用作事件处理程序的返回类型,前提是需在其中执行异步操作。winrt::fire_and_forget is also useful as the return type of your event handler when you need to perform asynchronous operations in it. 下面是一个示例(另请参阅 C++/WinRT 中的强引用和弱引用)。Here's an example (also see Strong and weak references in C++/WinRT).

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

第一个参数 (sender) 未命名,因为我们从未使用它。The first argument (the sender) is left unnamed, because we never use it. 因此,可以安全地将它保留为引用。For that reason we're safe to leave it as a reference. 但请注意,args 是按值传递的。But observe that args is passed by value. 请参阅上面的参数传递部分。See the Parameter-passing section above.

等待内核句柄Awaiting a kernel handle

C++/WinRT 提供一个 winrt::resume_on_signal 函数,用于在收到内核事件信号之前暂停。C++/WinRT provides a winrt::resume_on_signal function, which you can use to suspend until a kernel event is signaled. 你有责任确保在 co_await resume_on_signal(h) 返回之前句柄始终有效。You're responsible for ensuring that the handle remains valid until your co_await resume_on_signal(h) returns. resume_on_signal 本身不能为你执行该操作,因为在 resume_on_signal 开始之前,你就可能已失去句柄,如此示例(第一个示例)所示。resume_on_signal itself can't do that for you, because you may have lost the handle even before the resume_on_signal starts, as in this first example.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

传入的句柄仅在函数返回之前有效,该函数(为协同程序)在第一个暂停点(在此示例中为第一个 co_await)返回。The incoming HANDLE is valid only until the function returns, and this function (which is a coroutine) returns at the first suspension point (the first co_await in this case). 在等待 DoWorkAsync 时,控制返回到调用方,调用帧超出范围,你再也无法知道在协同程序继续时句柄是否会有效。While awaiting DoWorkAsync, control has returned to the caller, the calling frame has gone out of scope, and you no longer know whether the handle will be valid when your coroutine resumes.

从技术上来说,我们的协同程序在按值接收其参数,这符合预期(请参阅上面的参数传递)。Technically, our coroutine is receiving its parameters by value, as it should (see Parameter-passing above). 但在此示例中,我们需要更进一步,因此我们将遵循该指南的精神(而不仅仅是字面涵义)。But in this case we need to go a step further so that we're following the spirit of that guidance (rather than just the letter). 除了句柄,我们还需要传递强引用(换句话说,所有权)。We need to pass a strong reference (in other words, ownership) along with the handle. 操作方法如下。Here's how.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

通过值传递 winrt::handle 可以提供所有权语义,确保内核句柄在协同程序生存期内始终有效。Passing a winrt::handle by value provides ownership semantics, which ensures that the kernel handle remains valid for the lifetime of the coroutine.

下面介绍如何调用该协同程序。Here's how you might call that coroutine.

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
                other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

可以将超时值传递给 resume_on_signal,如本例所示。You can pass a timeout value to resume_on_signal, as in this example.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

异步超时变得很简单Asynchronous timeouts made easy

我们对 C++/WinRT 的 C++ 协同程序的投入很大。C++/WinRT is invested heavily in C++ coroutines. 协同程序对编写并发代码的影响是变革性的。Their effect on writing concurrency code is transformational. 就此部分讨论的示例来说,异步的详情并不重要,重要的是当场获得的结果。This section discusses cases where details of asynchrony are not important, and all you want is the result there and then. 因此,C++/WinRT 在实现 IAsyncAction Windows 运行时异步操作接口时会提供一个 get 函数(与 std::future 提供的 get 函数类似)。For that reason, C++/WinRT's implementation of the IAsyncAction Windows Runtime asynchronous operation interface has a get function, similar to that provided by std::future.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

当异步对象正在完成其操作时,get 函数会实施无限期的阻止。The get function blocks indefinitely, while the async object completes. 异步对象的生存期往往很短,因此通常情况下这正是你所需要的。Async objects tend to be very short-lived, so this is often all you need.

但有时候,你还有其他要求:在超过一定的时间后,你不能再等待。But there are cases where that's not sufficient, and you need to abandon the wait after some time has elapsed. 由于 Windows 运行时提供了构建基块,因此始终可以编写那样功能的代码。Writing that code has always been possible, thanks to the building blocks provided by the Windows Runtime. 不过,现在 C++/WinRT 提供的 wait_for 函数使之变得容易得多。But now C++/WinRT makes it a lot easier by providing the wait_for function. 它也在 IAsyncAction 上进行了实现。同样,它与 std::future 提供的 wait_for 函数类似。It's also implementated on IAsyncAction, and again it's similar to that provided by std::future.

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

备注

wait_for 在接口上使用 std::chrono::duration,但它有一个受限范围,该范围小于 std::chrono::duration 提供的值(大约为 49.7 天)。wait_for uses std::chrono::duration at the interface, but it is limited to some range smaller than what std::chrono::duration provides (roughly 49.7 days).

下面这个示例中的 wait_for 会先等待约五秒钟,然后检查完成情况。The wait_for in this next example waits for around five seconds and then it checks completion. 如果比较后得出的结果良好,则表明异步对象已成功完成,你的任务完成。If the comparison is favorable, then you know that the async object completed successfully, and you're done. 如果你在等待某个结果,则可随后调用 get 函数来检索结果。If you're waiting for some result, then you can simply follow that with a call to the get function to retrieve the result.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.get());
    }
}

由于异步对象其时已经完成,因此 get 函数会立即返回结果,不需进一步的等待。Because the async object has completed by then, get returns the result immediately, without any further wait. 可以看到,wait_for 返回了异步对象的状态。As you can see, wait_for returns the state of the async object. 因此,你可以将它用于更精细的控制,如下所示。So, you can use it for more fine-grained control, like this.

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.get());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • 记住,AsyncStatus::Completed 意味着异步对象成功完成,你可以调用 get 函数检索任何结果。Remember that AsyncStatus::Completed means that the async object completed successfully, and you may call the get function to retrieve any result.
  • AsyncStatus::Canceled 意味着异步对象已取消。AsyncStatus::Canceled means that the async object was canceled. 取消通常是由调用方请求的,因此需要处理该状态的情况很罕见。A cancellation is typically requested by the caller, so it would be rare to handle this state. 通常会直接将已取消的异步对象丢弃。Typically, a cancelled async object is simply discarded.
  • AsyncStatus::Error 意味着,从某些方面来看,异步对象已失败。AsyncStatus::Error means that the async object has failed in some way. 可以根据需要通过 get 重新引发异常。You can get to rethrow the exception if you wish.
  • AsyncStatus::Started 意味着异步对象仍在运行。AsyncStatus::Started means that the async object is still running. Windows 运行时异步模式不允许多个等待,也不允许多个等待程序。The Windows Runtime async pattern doesn't allow multiple waits, nor waiters. 这意味着不能以循环方式调用 wait_forThat means that you can't call wait_for in a loop. 如果等待实际上已超时,你可以有多个选择。If the wait has effectively timed-out, then you're left with a few choices. 可以放弃该对象,也可以轮询其状态,然后调用 get 来检索任何结果。You can abandon the object, or you can poll its status before calling get to retrieve any result. 不过,此时最佳选择是直接丢弃该对象。But it's best just to discard the object at this point.

异步返回数组Returning an array asynchronously

以下是 MIDL 3.0 示例,它生成错误 MIDL2025: [msg]syntax error [context]: expecting > or, near "["Below is an example of MIDL 3.0 that produces error MIDL2025: [msg]syntax error [context]: expecting > or, near "[".

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

原因在于将数组用作参数化接口的形参类型实参无效。The reason is that it's invalid to use an array as a parameter type argument to a parameterized interface. 因此,我们需要一种不太明显的方式来实现通过运行时类方法以异步方式传递回数组的目标。So we need a less obvious way to achieve the aim of asynchronously passing an array back from a runtime class method.

可以将装箱的数组返回到 PropertyValue 对象。You can return the array boxed into a PropertyValue object. 然后,调用代码会取消装箱。The calling code then unboxes it. 以下是一个代码示例,在此示例中,可以将 SampleComponent 运行时类添加到 Windows Runtime Component (C++/WinRT) 项目,然后从(例如) Core App (C++/WinRT) 项目使用它 。Here's a code example, which you can try out by adding the SampleComponent runtime class to a Windows Runtime Component (C++/WinRT) project, and then consuming that from (for example) a Core App (C++/WinRT) project.

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

重要的 APIImportant APIs