使用 C++/WinRT 的高级并发性和异步性

本主题介绍 C++/WinRT 中并发和异步的高级方案。

有关此主题的简介,请先阅读 并发和异步操作

将工作转交给 Windows 线程池

协同例程与任何其他函数的类似之处在于,调用方将会阻塞到某个函数向其返回了执行为止。 另外,协同例程返回的第一个机会是第一个 co_awaitco_returnco_yield

因此,在协同例程中执行受计算限制的工作之前,需要将执行返回给调用方(换句话说,引入暂停点),使调用方不被阻塞。 如果还没有对其他某个操作运行 co_await 来做到这一点,则可以对 winrt::resume_background 函数运行 co_await。 这会将控制权返回给调用方,然后立即在某个线程池线程上恢复执行。

在实现中使用的线程池是低级别Windows线程池,因此其效率最佳。

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

在编程时考虑线程亲和性

此方案扩展了上一个方案。 将一些工作卸载到线程池,但随后需要在用户界面(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 异常,因为必须从创建它的线程(即 UI 线程)更新 TextBlock 。 一种解决方案是捕获最初调用协同例程的线程上下文。 为此,请实例化 winrt::apartment_context 对象,执行后台工作,然后对 apartment_context 运行 co_await 以切回到调用上下文。

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 线程调用的,这种方法是可行的。 在你的应用中,很多情况下你都可以确定这一点。

有关更新 UI 的更常规解决方案(涵盖对调用线程不确定的情况),可以使用 co_awaitwinrt::resume_foreground 函数切换到特定的前台线程。 在下面的代码示例中,我们通过传递与 TextBlock 关联的调度程序队列(通过访问其 DispatcherQueue 属性)来指定前台线程。 winrt::resume_foreground 实现对该调度程序队列对象调用 DispatcherQueue.TryEnqueue,以执行协同例程中该调度程序对象之后的工作。

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.DispatcherQueue());

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

winrt::resume_foreground 函数采用可选的优先级参数。 如果使用的是该参数,则上面所示的模式是合适的。 如果没有,则可以选择简化 co_await winrt::resume_foreground(someDispatcherObject); 为只是 co_await someDispatcherObject;

协同例程中的执行上下文、恢复和切换

概括地说,在协同例程中某个暂停点之后,原始执行线程可能会消失,而恢复可能会在任何线程上发生(换而言之,任何线程都可以针对异步操作调用 Completed 方法)。

但是,如果你 co_await 四种 Windows 运行时 异步操作类型中的任意一种(IAsyncXxx),那么 C++/WinRT 会在你 co_await 时捕获调用上下文。 另外,它可以当延续操作恢复时,你仍处于该上下文中。 C++/WinRT 的做法是先检查你是否已经处于调用上下文中;如果没有,就切换到该上下文。 如果你在 co_await 之前位于单线程单元(STA)线程上,那么之后你仍会在同一个线程上;如果你在 co_await 之前位于多线程单元(MTA)线程上,那么之后你仍会在某个 MTA 线程上。

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++ 协程语言支持中(这些代码片段称为等待适配器)。 C++/WinRT 中剩余的可等待类型只是一些线程池包装器和/或帮助器;因此它们会在线程池中完成。

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 协同例程实现中,则另一个库会提供适配器,你需要了解这些适配器在恢复和上下文方面的作用。

若要将上下文切换保持在最低水平,可以使用本主题中已介绍的一些技术。 我们来看几个这样的示例。 在此下一个伪代码示例中,我们演示了一个事件处理程序的大纲,该事件处理程序调用Windows 运行时 API 以加载图像,将放置到后台线程以处理该图像,然后返回到 UI 线程以显示 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->DispatcherQueue());

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

    // Display the image in the UI.
}

对于此方案,对 StorageFile::OpenAsync 的调用有点效率低下。 有一个必要的上下文切换到后台线程(以便处理程序可以将执行返回到调用方),在恢复后 C++/WinRT 将还原 UI 线程上下文。 但是,在此情况下,在我们即将更新 UI 之前,没有必要处于 UI 线程中。 在调用 winrt::resume_background 之前调用的 Windows 运行时 API 越多,发生的不必要往返上下文切换也越多。 解决方案在此之前不调用任何Windows 运行时 API。 将它们全部移到 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->DispatcherQueue());

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

    // Display the image in the UI.
}

如果你想做一些更高级的操作,那么你可以编写自己的 await 适配器。 例如,如果你希望 co_await 在异步操作完成时所在的同一线程上恢复执行(这样就不会发生上下文切换),那么可以先编写类似于下面所示的 await 适配器。

注释

下面的代码示例仅用于教育目的;它让你开始了解 await 适配器的工作原理。 如果您想在自己的代码库中使用这种技术,我们建议您自行开发和测试一个或多个 await 适配器结构体。 例如,可以编写 complete_on_anycomplete_on_currentcomplete_on(dispatcher)。 此外,请考虑将它们设置为将 IAsyncXxx 类型作为模板参数的模板。

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的函数。 C++/WinRT 库提供这些函数,以便你默认获得合理的行为,如下所示。

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

若要使用 no_switch await 适配器,只需将该 co_await 表达式的类型从 IAsyncXxx 更改为 no_switch,如下所示。

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

然后,C++ 编译器不再查找与 IAsyncXxx 匹配的三个 await_xxx 函数,而是查找与 no_switch 匹配的函数。

深入了解 winrt::resume_foreground

C++/WinRT 2.0 开始,winrt::resume_foreground 函数会暂停,即使是从调度程序线程来调用它(在以前的版本中,它可能会在某些情况下引入死锁,因为它暂停的前提是尚未位于调度程序线程上)。

当前的行为意味着,你可以依赖于堆栈展开和进行的重新排队;这对于系统稳定很重要,尤其是在低级别系统代码中。 在上文的 考虑线程关联性的编程 一节中,最后一个代码清单展示了如何在后台线程上执行一些复杂计算,然后切换到适当的 UI 线程来更新用户界面(UI)。

以下是 winrt::resume_foreground 的内部实现。

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 之间的差异。 PostMessage 将工作排成队列,然后展开堆栈,而无需等待工作完成。 堆栈展开有时候很重要。

winrt::resume_foreground 函数最初支持 CoreDispatcher(绑定到 CoreWindow),这是在Windows 10之前引入的。 在 WinUI 3 和Windows 应用 SDK应用中,请改用 DispatcherQueue。 可以出于自己的目的创建 DispatcherQueue 。 请考虑此简单的控制台应用程序。

using namespace Windows::System;

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

上面的示例在专用线程上创建一个队列(包含在控制器中),然后将控制器传递给协同程序。 协同程序可以使用队列在专用线程上等待(先暂停再继续)。 DispatcherQueue 的另一个常见用途是为传统桌面或 Win32 应用在当前 UI 线程上创建队列。

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 对象传输到调用方。 这也正是你可以在现有的 Petzold 风格 Win32 桌面应用程序中支持高效且无缝的队列处理的方法。

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

上面的简单 函数首先创建一个窗口。 可以想象,这会注册一个窗口类,并调用 CreateWindow 来创建顶级桌面窗口。 然后调用 CreateDispatcherQueueController 函数来创建队列控制器,再通过该控制器拥有的调度程序队列调用某个协同程序。 然后进入传统的消息泵,在其中的此线程上自然而然地恢复协同程序。 然后,你可以回到协同程序所在的位置,在应用程序中完成异步的或基于消息的工作流。

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 时会始终先排队,然后展开堆栈。 还可以选择设置恢复优先级。

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

或者,使用默认队列顺序。

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

注释

如上所示,请确保包含要 co_await 的类型的命名空间的投影标头。 例如,Windows::System::DispatcherQueueMicrosoft::UI::Dispatching::DispatcherQueue

或者,检测队列关闭情况并对其进行适当处理,如以下示例所示。

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

co_await 表达式返回 true,表明会在调度程序线程上进行恢复。 换而言之,该排队操作是成功的。 相反,它会返回 false 以指示执行保留在调用线程上,因为队列的控制器正在关闭并且不再为队列请求提供服务。

因此,在将 C++/WinRT 与协同程序配合使用的情况下,尤其是在进行一些传统的 Petzold 样式桌面应用程序开发的情况下,你拥有很大的控制权限。

取消异步操作和取消回调

使用 Windows 运行时的异步编程功能可以取消正在进行的异步操作或运算。 以下示例调用 StorageFolder::GetFilesAsync 以检索可能较大的文件集合,并将生成的异步操作对象存储在数据成员中。 用户可以选择取消操作。

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

// MainPage.h
...
#include <winrt/Windows.Foundation.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 Microsoft::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;
};
...

对于取消的实现端,让我们从一个简单的示例开始。

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

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

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

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

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

如果你运行上面的示例,就会看到 ImplicitCancelationAsync 以每秒一条消息的频率打印,持续三秒,之后它会由于被取消而自动终止。 之所以此行为是可行的,是因为在遇到 co_await 表达式时,协同例程会检查它是否已取消。 如果已取消,则它会短路掉,否则它会像正常情况下一样暂停。

当然,取消也可以在协同例程暂停时发生。 仅当协同例程恢复或者遇到另一个 co_await 时,它才会检查取消状态。 问题在于响应取消请求时的延迟粒度可能过粗。

因此,另一种做法是从协同例程内部显式轮询取消。 使用以下列表中的代码更新上面的示例。 在这个新示例中,ExplicitCancelationAsync 获取 winrt::get_cancellation_token 函数返回的对象,并使用它定期检查协程是否已取消。 只要尚未取消,协同例程就会无限循环;一旦取消,循环和函数就会正常退出。 结果与前面的示例相同,但此处的退出是显式发生的,并且处于控制之下。

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

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

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

等待 winrt::get_cancellation_token 时会使用协同例程代表你生成的 IAsyncAction 信息检索取消标记。 可以针对该标记使用函数调用运算符以查询取消状态,实质上是轮询取消。 如果要执行一些计算绑定操作或循环访问大型集合,则这是一种合理的技术。

注册取消回调

Windows 运行时 的取消不会自动传播到其他异步对象。 但是,可以注册取消回调,这是 Windows SDK 版本 10.0.17763.0(Windows 10 版本 1809)中引入的功能。 这是一种先发性的挂钩,可据此传播取消,以及与现有的并发库集成。

在下一个代码示例中, NestedCoroutineAsync 会执行该工作,但其中没有特殊的取消逻辑。 CancelationPropagatorAsync 实质上是协同例程中的一个包装器;该包装器提前转发取消。

// 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 CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

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

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

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

CancelationPropagatorAsync 为自身的取消回调注册一个 lambda 函数,然后等待(此时会暂停)嵌套的工作完成。 如果 CancellationPropagatorAsync 已取消,则会将取消传播到嵌套的协同例程。 无需轮询取消;取消不会无限期阻塞。 此机制足够灵活,可用于与一个对 C++/WinRT 一无所知的协同例程或并发库进行互操作。

报告进度

如果协同例程返回 IAsyncActionWithProgressIAsyncOperationWithProgress,则你可以检索 winrt::get_progress_token 函数返回的对象,并使用该对象将进度报告给进度处理程序。 下面是一个代码示例。

// 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.set_result(pi_so_far);
    progress(0.2);

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

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

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

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    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 << L". "
                   << L"Value so far: " << sender.GetResults() << 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();
}

若要报告进度,请使用进度值作为参数调用进度标记。 若要设置临时结果,请在 set_result() 进度令牌上使用该方法。

注释

报告临时结果需要 C++/WinRT 版本 2.0.210309.3 或更高版本。

上面的示例选择为每个进度报告设置临时结果。 可以选择随时报告临时结果(如果有)。 它不需要与进度报告结合。

注释

为异步动作或操作实现一个以上的 完成处理程序 是不正确的。 可对其已完成的事件使用单个委托,或者可对其运行 co_await。 如果你有这两个,则第二个将失败。 以下两种完成处理程序中的任意一种都可以使用;但对于同一个异步对象,不能同时使用两者。

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

有关完成处理程序的详细信息,请参阅 异步操作和操作的委托类型

发后不理

有时,你有一个可以与其他工作同时完成的任务,你不需要等待该任务完成(没有其他工作依赖于它),也不需要它返回值。 在这种情况下,可以激发该任务,然后忘记它。 为此,可以编写返回类型为 winrt::fire_and_forget(而不是某个 Windows 运行时异步操作类型,或 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 作为其返回类型也很有用。 下面是一个示例(另请参阅 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.
}

第一个参数( 发送方)未命名,因为我们永远不会使用它。 因此,我们安全地将其保留为参考。 但请注意,args 是按值传递的。 请参阅上面的 参数传递 部分。

等待内核句柄

C++/WinRT 提供 winrt::resume_on_signal 函数,可用于暂停,直到发出内核事件信号。 你有责任确保在 co_await resume_on_signal(h) 返回之前句柄始终有效。 resume_on_signal 本身不能为你执行该操作,因为在 resume_on_signal 开始之前,你就可能已失去句柄,如此示例(第一个示例)所示。

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

传入的句柄仅在函数返回之前有效,该函数(为协同程序)在第一个暂停点(在此示例中为第一个 co_await)返回。 在等待 DoWorkAsync 时,控制返回到调用方,调用帧超出范围,你再也无法知道在协同程序继续时句柄是否会有效。

严格来说,我们的协程正如预期的那样,以值传递方式接收参数(参见上文参数传递)。 但在这种情况下,我们需要更进一步,以确保我们遵循的是该指导意见的精神(而不仅仅是字面意思)。 除了句柄,我们还需要传递强引用(换句话说,所有权)。 操作方法如下。

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

通过值传递 winrt::handle 可以提供所有权语义,确保内核句柄在协同程序生存期内始终有效。

下面介绍如何调用该协同程序。

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,如以下示例所示。

winrt::handle event = ...

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

异步超时变得很简单

我们对 C++/WinRT 的 C++ 协同程序的投入很大。 它们对编写并发代码的影响是转换性的。 本部分讨论这样一些情况:异步处理的细节并不重要,你只想当场得到结果。 因此,C++/WinRT 实现 IAsyncAction Windows 运行时异步操作接口具有 get 函数,类似于 std::future 提供的函数

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

get 函数会无限期阻塞,直到异步对象完成。 异步对象往往生存期非常短,因此这通常是你需要的。

但在某些情况下,这还不够,你需要在一段时间过后放弃等待。 由于Windows 运行时提供的构建基块,编写该代码始终可能。 但现在,C++/WinRT 通过提供 wait_for 函数,让这一过程变得容易得多。 它还在 IAsyncAction 上实现,同样类似于 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 等待大约 5 秒,然后检查完成情况。 如果比较结果符合预期,那么你就知道该异步对象已成功完成,这样就可以了。 如果等待某个结果,只需调用 GetResults 方法来检索结果即可。

注释

wait_forget 是互斥的(不能同时调用二者)。 它们各自都算作一个等待者,而 Windows 运行时 异步操作和操作对象仅支持单个等待者。

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

由于异步对象已完成, 因此 GetResults 方法会立即返回结果,无需再等待。 如你所看到的, wait_for 返回异步对象的状态。 因此,你可以使用它进行更精细的控制,如下所示。

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • 请记住 ,AsyncStatus::Completed 表示异步对象已成功完成,可以调用 GetResults 方法来检索任何结果。
  • AsyncStatus::Canceled 表示已取消异步对象。 取消通常由调用方发起,因此很少需要处理这种状态。 通常,已取消的异步对象会被直接丢弃。 如果需要,可以调用 GetResults 方法来重新引发取消异常。
  • AsyncStatus::Error 表示异步对象在某些方面失败。 如果需要,可以调用 GetResults 方法来重新引发异常。
  • AsyncStatus::Started 表示异步对象仍在运行。 Windows 运行时 的异步模式不允许多次等待,也不允许多个等待方。 这意味着不能在循环中调用 wait_for 。 如果等待实际上已超时,你可以有多个选择。 可以放弃对象,也可以在调用 GetResults 方法检索任何结果之前轮询其状态。 但这时最好直接丢弃该对象。

另一种做法是仅检查 Started,并让 GetResults 处理其他情况。

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

异步返回一个数组

以下是 MIDL 3.0 示例,它生成错误 MIDL2025: [msg]syntax error [context]: expecting > or, near "["

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

原因是将数组用作参数化接口的参数类型参数无效。 因此,我们需要一种不太明显的方法来实现从运行时类方法异步传递数组的目标。

可以返回封装在 PropertyValue 对象中的数组。 然后,调用代码会取消装箱。 下面是一个代码示例,你可以尝试将 SampleComponent 运行时类添加到 Windows 运行时 Component (C++/WinRT) 项目中,然后在(例如)Blank App, Packaged (WinUI 3 in Desktop) 项目中使用它。

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

重要 API