异步,以及 C++/WinRT 与 C++/CX 之间的互操作

Tip

尽管我们建议你从头开始阅读本主题,但你也可以直接跳到 将 C++/CX 异步移植到 C++/WinRT 概述 一节,查看互操作技术摘要。

这是一个与从 C++/CX 逐步移植到 C++/WinRT 相关的高级主题。 本主题接续C++/WinRT 与 C++/CX 之间的互操作主题未完的内容。

如果代码库的大小或复杂性使得需要逐步移植项目,则需要一个移植过程,在该过程中,C++/CX 和 C++/WinRT 代码在同一项目中并排存在。 如果你拥有异步代码,则可能需要在逐步移植源代码的过程中,让并行模式库 (PPL) 任务链和协同程序在项目中并存。 本主题重点介绍在异步 C++/CX 代码和异步 C++/WinRT 代码之间进行互操作的技术。 可以单独使用这些技术,也可以一起使用。 这些技术使你能够在将整个项目逐步移植的过程中,进行渐进式、可控的局部修改,而不会让每一次修改都在整个项目中引发失控的连锁反应。

在阅读本主题之前,建议先阅读 C++/WinRT 与 C++/CX 之间的互操作。 本主题介绍如何准备项目以便逐步移植。 它还引入了两个帮助程序函数,可用于将 C++/CX 对象转换为 C++/WinRT 对象(反之亦然)。 本主题的异步内容基于这些信息,并使用这些帮助程序函数。

注释

从 C++/CX 逐步移植到 C++/WinRT 存在一些限制。 如果你有一个Windows 运行时 组件项目,就无法逐步迁移,而需要一次性完成整个项目的迁移。 对于 XAML 项目,在任一时刻,你的 XAML 页面类型都必须要么全部为 C++/WinRT,要么全部为 C++/CX。 有关详细信息,请参阅主题 “从 C++/CX 移动到 C++/WinRT”。

整个主题专用于异步代码互操作的原因

从 C++/CX 到 C++/WinRT 的移植通常很简单,但从并行模式库 (PPL) 任务迁移到协同程序的情况例外。 模型不同。 从 PPL 任务到协同程序没有自然的一对一映射,也没有适用于所有情况的机械移植代码的简单方法。

好消息是,将任务转换为协程可以大幅简化代码。 开发团队经常报告,一旦他们越过移植异步代码的障碍,移植工作的其余部分基本上是机械的。

通常,算法最初是为了适合同步 API 而编写的。 其随后转换为任务和显式延续,结果通常是对底层逻辑的无意识混淆。 例如,循环变为递归;if-else 分支变成嵌套的任务树(链);共享变量会变成 shared_ptr。 若要解构 PPL 源代码的通常非自然结构,建议先退一步,了解原始代码的意图(即发现原始同步版本)。 然后将 co_await(协同等待)插入适当位置。

因此,如果你有一个 C#(而不是 C++/CX)版本的异步代码可作为移植起点,那么移植起来会更容易,也能得到一个更简洁的移植版本。 C# 代码使用 await。 因此,C# 代码基本上遵循从同步版本开始,然后插入 await 到适当位置的理念。

如果没有项目的 C# 版本,则可以使用本主题中所述的技术。 移植到 C++/WinRT 后,如果希望移植到 C#,异步代码的结构将更容易移植到 C# 。

异步编程中的一些背景

为了让我们对异步编程的概念和术语有一个共同的参照框架,先简要介绍一下 Windows 运行时 异步编程的总体背景,以及两种 C++ 语言投影分别以不同方式构建于其之上的情况。

你的项目具有异步工作的方法,并且有两种主要类型。

  • 通常想要等待异步工作完成,然后再执行其他操作。 返回异步操作对象的方法即可以等待异步工作完成的方法。
  • 但有时你不希望或需要等待异步完成的工作。 在这种情况下,异步方法 返回异步操作对象的效率更高。 你无需等待的异步方法称为发后不理 (fire-and-forget) 方法

Windows 运行时异步对象(IAsyncXxx

Windows::Foundation Windows 运行时命名空间包含四种类型的异步操作对象。

在本主题中,当我们使用方便的简写 IAsyncXxx 时,可能是统称这些类型;也可能是指四种类型中的某一种,而无需明确说明具体是哪一种。

C++/CX 异步

C++/CX 异步代码使用并行模式库 (PPL) 任务。 PPL 任务由 并发::task 类表示。

通常,异步 C++/CX 方法通过将 lambda 函数与 concurrency::create_taskconcurrency::task::then 配合使用,将 PPL 任务串联起来。 每个 lambda 函数返回一个任务,该任务完成后会生成一个值,然后传递到任务的 延续的 lambda 中。

或者,异步 C++/CX 方法可以不调用 create_task 来创建任务,而调用 concurrency::create_async 来创建一个 IAsyncXxx^。

因此,异步 C++/CX 方法的返回类型可以是 PPL 任务或 IAsyncXxx^。

在任一情况下,方法本身都使用 return 关键字返回异步对象,该对象在完成时会生成调用方实际想要的值(可能是文件、字节数组或布尔值)。

注释

如果异步 C++/CX 方法返回 IAsyncXxx^,则 TResult(如果有)仅限于Windows 运行时类型。 例如,布尔值是 Windows 运行时 类型;但 C++/CX 投影类型(例如 Platform::Array<byte>^)则不是。

C++/WinRT 异步

C++/WinRT 将 C++ 协程集成到编程模型中。 协同程序和 co_await 语句提供一种协同等待结果的自然方法。

每个 IAsyncXxx 类型都投影到 winrt::Windows::Foundation C++/WinRT 命名空间中的对应类型。 让我们将其称为 winrt::IAsyncXxx (与 C++/CX 的 IAsyncXxx^ 相比)。

C++/WinRT 协同程序的返回类型为 winrt::IAsyncXxxwinrt::fire_and_forget。 与使用 return 关键字返回异步对象相反,协同例程使用 co_return 关键字协作返回调用方实际想要的值(可能是文件、字节数组或布尔值)。

如果某个方法包含至少一个 co_await 语句(或至少一个 co_returnco_yield),那么该方法就因此是一个协程。

有关详细信息和代码示例,请参阅 使用 C++/WinRT 的并发和异步操作

Direct3D 游戏示例 (Simple3DGameDX

本主题包含几个特定编程技术的演练,这些技术说明了如何逐步移植异步代码。 为了作为案例研究,我们将使用 Direct3D 游戏示例 的 C++/CX 版本(称为 Simple3DGameDX)。 我们将展示一些示例,说明如何获取该项目中的原始 C++/CX 源代码,并逐渐将其异步代码移植到 C++/WinRT。

  • 从上面的链接下载 ZIP,然后将其解压缩。
  • 在Visual Studio中打开 C++/CX 项目(位于命名cpp的文件夹中)。
  • 然后,需要向项目添加 C++/WinRT 支持。 执行该操作所需遵循的步骤在 将 C++/WinRT 支持添加到 C++/CX 项目中 一文中有说明。 在该部分中,关于将 interop_helpers.h 头文件添加到你的项目中的这一步尤其重要,因为在本主题中我们将依赖这些辅助函数。
  • 最后,将 #include <pplawait.h> 添加到 pch.h 中。 此操作提供对 PPL 的协同程序支持(下一部分中将详细介绍该支持)。

暂时不要生成,否则你将收到关于 byte 不明确的错误。 下面介绍如何解决此问题。

  • 打开 BasicLoader.cpp,并注释掉 using namespace std;
  • 在该源代码文件中,需要将 shared_ptr 限定为 std::shared_ptr。 你可以在该文件中进行查找和替换来做到这一点。
  • 然后将 矢量 限定为 std::vector,将 字符串 限定为 std::string

该项目现已可再次成功生成,支持 C++/WinRT,并包含 from_cxto_cx 互操作帮助函数。

现在,你已准备好 Simple3DGameDX 项目,可用于本主题中的代码演练。

将 C++/CX 异步移植到 C++/WinRT 的概述

简言之,在移植过程中,我们会将 PPL 任务链改写为对 co_await 的调用。 我们将将方法的返回值从 PPL 任务更改为 C++/WinRT winrt::IAsyncXxx 对象。 我们还会将任何 IAsyncXxx^ 更改为 C++/WinRT winrt::IAsyncXxx

你会记得,协同程序是调用 co_xxx 的任何方法。 C++/WinRT 协程使用 co_return 来以协作方式返回其值。 借助对 PPL 的协同程序支持(由 pplawait.h 提供),还可以使用 co_return 从协同程序返回 PPL 任务。 你还可以 co_await 任务和 IAsyncXxx。 但不能使用 co_return 来返回 IAsyncXxx^。 下表描述对图片中使用 pplawait.h 的各种异步方法之间的互操作的支持。

方法 你能 co_await 吗? 能否从中 co_return
方法返回 task<void> Yes Yes
方法返回 Task<T> No Yes
方法返回 IAsyncXxx^ Yes No. 但是,你可以让 create_async 环绕使用 co_return 的任务。
方法返回 winrt::IAsyncXxx Yes Yes

使用下表直接跳转到本主题中介绍感兴趣的互操作技术的部分,或直接从此处继续阅读。

异步互操作技术 本主题中的章节
使用 co_await 在发后不理方法或构造函数中等待 task<void> 方法 在发后不理方法中等待 task<void>
使用 co_await 在 task<void> 方法中等待 task<void> 方法 task<void> 方法中等待 task<void>
使用 co_await 在 task<T> 方法中等待 task<void> 方法 task<T> 方法中等待 task<void>
使用 co_await 等待 IAsyncXxx^ 方法。 task方法中等待IAsyncXxx^,而项目的其余部分保持不变
在 task<void> 方法中使用 co_return task<void> 方法中等待 task<void>
任务<T> 方法内使用 co_return task方法中等待 IAsyncXxx^,而使项目的其余部分保持不变
让 create_async 环绕使用 co_return 的任务 create_async 环绕使用 co_return 的任务
移植 concurrency::wait concurrency::wait 移植到 co_await winrt::resume_after
返回 winrt::IAsyncXxx,而不是 task<void> task<void> 返回类型移植到 winrt::IAsyncXxx
winrt::IAsyncXxx<T>(T 为基本类型)转换为 task<T> winrt::IAsyncXxx<T>(T 是基本类型)转换为 task<T>
winrt::IAsyncXxx<T>(T 是 Windows 运行时 类型)转换为 task<T^> winrt::IAsyncXxx<T>(T 是 Windows 运行时 类型)转换为 task<T^>

下面是一个演示某些支持的简短代码示例。

#include <ppltasks.h>
#include <pplawait.h>
#include <winrt/Windows.Foundation.h>

concurrency::task<bool> TaskAsync()
{
    co_return true;
}

Windows::Foundation::IAsyncOperation<bool>^ IAsyncXxxCppCXAsync()
{
    // co_return true; // Error! Can't do that. But you can do
    // the following.
    return concurrency::create_async([=]() -> concurrency::task<bool> {
        co_return true;
        });
}

winrt::Windows::Foundation::IAsyncOperation<bool> IAsyncXxxCppWinRTAsync()
{
    co_return true;
}

concurrency::task<bool> CppCXAsync()
{
    bool b1 = co_await TaskAsync();
    bool b2 = co_await IAsyncXxxCppCXAsync();
    co_return co_await IAsyncXxxCppWinRTAsync();
}

winrt::fire_and_forget CppWinRTAsync()
{
    bool b1 = co_await TaskAsync();
    bool b2 = co_await IAsyncXxxCppCXAsync();
    bool b3 = co_await IAsyncXxxCppWinRTAsync();
}

Important

即使有了这些出色的互操作选项,要实现逐步移植,仍取决于选择那些可以精准实施且不会影响项目其他部分的改动。 我们希望避免任意抓住一头不放,因此拆解了整个项目的结构。 为此,我们必须按特定顺序做事。 接下来,我们将详细看一看一些进行这类异步相关移植/互操作更改的示例。

等待 task<void> 方法,项目其余部分保持不变

返回 任务<void> 的方法以异步方式执行工作,并返回异步操作对象,但最终不会生成值。 我们可以 co_await 这样的方法。

因此,开始逐步移植异步代码的好位置是查找调用此类方法的位置。 这些位置将涉及到创建和/或返回任务。 它们还可能涉及这样一种任务链:每个任务都不会将任何值传递给其后续操作。 在这样的位置,只需将异步代码替换为 co_await 语句,正如我们看到的。

注释

随着本主题的进行,你将看到此策略的好处。 一旦某个特定的 task<void> 方法开始仅通过 co_await 调用,你就可以将该方法移植到 C++/WinRT,并让它返回 winrt::IAsyncXxx

让我们找到一些示例。 打开 Simple3DGameDX 项目(请参阅 Direct3D 游戏示例)。

Important

在下面的示例中,当你看到方法的实现发生更改时,请记住,我们不需要更改我们更改的方法的 调用方 。 这些更改已本地化,它们不会在项目中级联。

在发后不理方法中等待 task<void>

让我们从在发后不理方法中等待 task<void> 开始,因为这是最简单的情况。 这些是异步工作的方法,但该方法的调用方不会等待该工作完成。 只需调用该方法并忘记该方法,尽管该方法以异步方式完成。

查看项目依赖项关系图的根目录,查找包含 create_task 的 void 方法和/或仅调用 task<void> 方法的任务链

Simple3DGameDX 中,你将在 GameMain::Update 方法的实现中找到类似代码。 它位于源代码文件中 GameMain.cpp

GameMain::Update

下面摘录的是该方法的 C++/CX 版本,展示了该方法中以异步方式完成的两个部分。

void GameMain::Update()
{
    ...
    case UpdateEngineState::WaitingForPress:
        ...
        m_game->LoadLevelAsync().then([this]()
        {
            m_game->FinalizeLoadLevel();
            m_updateState = UpdateEngineState::ResourcesLoaded;
        }, task_continuation_context::use_current());
        ...
    case UpdateEngineState::Dynamics:
        ...
        m_game->LoadLevelAsync().then([this]()
        {
            m_game->FinalizeLoadLevel();
            m_updateState = UpdateEngineState::ResourcesLoaded;
        }, task_continuation_context::use_current());
        ...
    ...
}

可以看到调用了 Simple3DGame::LoadLevelAsync 方法(该方法返回一个 PPL task<void>)。 之后是执行一些同步工作的延续LoadLevelAsync 是异步的,但它不返回值。 因此,没有任何值从任务传递到延续。

我们可以在这两个位置对代码进行相同的更改。 代码将在下面的清单后进行解释。 我们本可在此讨论在类成员协同程序中访问 this 指针的安全方法。 但是,我们把这个留到后面的章节再讨论吧(关于 co_awaitthis 指针的延后讨论)——不过现在,这段代码可以正常工作。

winrt::fire_and_forget GameMain::Update()
{
    ...
    case UpdateEngineState::WaitingForPress:
        ...
        co_await m_game->LoadLevelAsync();
        m_game->FinalizeLoadLevel();
        m_updateState = UpdateEngineState::ResourcesLoaded;
        ...
    case UpdateEngineState::Dynamics:
        ...
        co_await m_game->LoadLevelAsync();
        m_game->FinalizeLoadLevel();
        m_updateState = UpdateEngineState::ResourcesLoaded;
        ...
    ...
}

如你所见,因为 LoadLevelAsync 返回任务,所有我们可以 co_await 它。 而且我们不需要显式的延续操作——跟在 co_await 后面的代码只有在 LoadLevelAsync 完成后才会执行。

引入 co_await 会使该方法变成协程,因此我们不能再让它返回 void。 这是发后不理方法,因此我们将其更改为返回 winrt::fire_and_forget

还需要编辑 GameMain.h。 在此处的声明中,将 GameMain::Update 的返回类型更改为 voidwinrt::fire_and_forget

你可以在你的项目副本中进行此更改,游戏仍然可以像以前一样构建和运行。 源代码仍然是 C++/CX,但它现在使用与 C++/WinRT 相同的模式,因此,这让我们更接近于能够以机械方式移植其余代码。

GameMain::ResetGame

GameMain::ResetGame 是另一种即发即弃的方法,它也会调用 LoadLevelAsync。 因此,如果你想练习一下,也可以在那里做同样的代码修改。

GameMain::OnDeviceRestored

事情在 GameMain::OnDeviceRestored 中变得更加有趣,因为它更深入地嵌套了异步代码,包括无操作任务。 下面是该方法中异步部分的概述(省略号表示其余较乏味的同步代码)。

void GameMain::OnDeviceRestored()
{
    ...
    create_task([this]()
    {
        return m_renderer->CreateGameDeviceResourcesAsync(m_game);
    }).then([this]()
    {
        ...
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            ...
            return m_game->LoadLevelAsync().then([this]()
            {
                ...
            }, task_continuation_context::use_current());
        }
        else
        {
            return create_task([]()
            {
                // Return a no-op task.
            });
        }
    }, task_continuation_context::use_current()).then([this]()
    {
        ...
    }, task_continuation_context::use_current());
}

首先,在 GameMain.h.cpp 中,将 GameMain::OnDeviceRestored 的返回类型从 void 更改为 winrt::fire_and_forget。 还需要打开 DeviceResources.h 并更改 IDeviceNotify::OnDeviceRestored 的返回类型。

若要移植异步代码,请删除所有 create_task然后 调用及其大括号,并将该方法简化为一系列平面语句。

将任何返回任务的 return 更改为 co_await。 最后会剩下一个不返回任何内容的 return,那就直接删除它即可。 完成后,无操作任务将消失,方法异步部分的概览如下所示。 同样,没那么有趣的同步代码也被省略了。

winrt::fire_and_forget GameMain::OnDeviceRestored()
{
    ...
    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
    ...
    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        co_await m_game->LoadLevelAsync();
        ...
    }
    ...
}

正如你所看到的,这种异步结构形式要简单得多,更易于阅读。

GameMain::GameMain

GameMain::GameMain 构造函数异步执行工作,项目没有任何部分等待该工作完成。 同样,此列表概述了异步部分。

GameMain::GameMain(...) : ...
{
    ...
    create_task([this]()
    {
        ...
        return m_renderer->CreateGameDeviceResourcesAsync(m_game);
    }).then([this]()
    {
        ...
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            return m_game->LoadLevelAsync().then([this]()
            {
                ...
            }, task_continuation_context::use_current());
        }
        else
        {
            return create_task([]()
            {
                // Return a no-op task.
            });
        }
    }, task_continuation_context::use_current()).then([this]()
    {
        ....
    }, task_continuation_context::use_current());
}

但是构造函数无法返回 winrt::fire_and_forget,因此我们将异步代码移动到新的 GameMain::ConstructInBackground fire-and-forget 方法中,将代码平展为 co_await 语句,并从构造函数调用新方法。 下面是结果。

GameMain::GameMain(...) : ...
{
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
    ...
    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        ...
        co_await m_game->LoadLevelAsync();
        ...
    }
    ...
}

现在,GameMain 中的所有发后不理方法(实际上是所有异步代码)都变成了协同程序。 如果你有这样的倾向,也许可​​以在其他类中寻找发后不理方法,并进行类似更改。

关于 co_awaitthis 指针的延后讨论

当我们更改 GameMain::Update 时,我推迟了 有关此 指针的讨论。 让我们在这里讨论一下。

这适用于我们到目前为止更改的所有方法;它适用于所有协同程序,而不仅仅是发后不理协同程序。 将 co_await 引入方法会引入暂停点。 因此,我们必须小心使用 this 指针,当然,每次访问类成员时,我们都会在暂停点之后使用它。

简言之,解决方案是调用 implements::get_strong。 但是,有关该问题和解决方案的完整讨论,请参阅在类成员协同程序中安全访问 this 指针

只能在派生自 winrt::implements 的类中调用 implements::get_strong

winrt::implements 派生 GameMain

我们需要做出的第一个更改是在 GameMain.h

class GameMain :
    public DX::IDeviceNotify

GameMain 将继续实现 DX::IDeviceNotify,但我们将将其更改为派生自 winrt::implements

class GameMain : 
    public winrt::implements<GameMain, winrt::Windows::Foundation::IInspectable>,
    DX::IDeviceNotify

接下来,你会在 App.cpp 中找到这个方法。

void App::Load(Platform::String^)
{
    if (!m_main)
    {
        m_main = std::unique_ptr<GameMain>(new GameMain(m_deviceResources));
    }
}

但是,现在 GameMain 派生自 winrt::implements,我们需要以不同的方式构造它。 在本例中,我们将使用 winrt::make_self 函数模板。 有关详细信息,请参阅 实例化和返回实现类型和接口

用这行代码替换那行代码。

    ...
    m_main = winrt::make_self<GameMain>(m_deviceResources);
    ...

若要关闭该更改的循环,我们还需要更改 m_main的类型。 在 App.h 中,你会找到这段代码。

ref class App sealed :
    public Windows::ApplicationModel::Core::IFrameworkView
{
    ...
private:
    ...
    std::unique_ptr<GameMain> m_main;
};

m_main 的声明改为如下内容。

    ...
    winrt::com_ptr<GameMain> m_main;
    ...

现在可以调用 implements::get_strong

对于 GameMain::Update,以及我们添加了 co_await 的任何其他方法,下面介绍如何在协同程序开始时调用 get_strong,以确保强引用保留到协同程序完成为止。

winrt::fire_and_forget GameMain::Update()
{
    auto strong_this{ get_strong() }; // Keep *this* alive.
    ...
        co_await ...
    ...
}

在 task<void> 方法中等待 task<void>

下一个最简单的情况是在本身返回 task<void> 的方法中等待 task<void>。 因为我们可以 co_await task<void>,并且可以从其 co_return

Simple3DGame::LoadLevelAsync 方法的实现中可以找到一个非常简单的示例。 它位于源代码文件中 Simple3DGame.cpp

task<void> Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    return m_renderer->LoadLevelResourcesAsync();
}

只是一些同步代码,随后返回由 GameRenderer::LoadLevelResourcesAsync 创建的任务。

我们不返回该任务,而是 co_await,然后 co_return 生成的 void

task<void> Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    co_return co_await m_renderer->LoadLevelResourcesAsync();
}

这看起来不像是一个深刻的变化。 但现在,我们通过 co_await 调用 GameRenderer::LoadLevelResourcesAsync 后,就可以自由对其进行移植,以返回 winrt::IAsyncXxx 而不是任务。 稍后我们将在将 task<void> 返回类型移植到 winrt::IAsyncXxx 部分中执行此操作

在 task<T> 方法中等待 task<void>

虽然 Simple3DGameDX 中没有合适的示例,但我们可以用假设的示例来显示模式。

下面的代码示例中的第一行演示了 co_awaittask<void> 的简单用法。 然后,为了满足 任务<T> 返回类型,我们需要异步返回 StorageFile^。 为此,我们需要 co_await Windows 运行时 API,并 co_return 生成文件。

task<StorageFile^> Simple3DGame::LoadLevelAndRetrieveFileAsync(
    StorageFolder^ location,
    Platform::String^ filename)
{
    co_await m_renderer->LoadLevelResourcesAsync();
    co_return co_await location->GetFileAsync(filename);
}

我们甚至可以将更多方法移植到 C++/WinRT,如下所示。

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
Simple3DGame::LoadLevelAndRetrieveFileAsync(
    StorageFolder location,
    std::wstring filename)
{
    co_await m_renderer->LoadLevelResourcesAsync();
    co_return co_await location.GetFileAsync(filename);
}

该示例中m_renderer数据成员仍然是 C++/CX。

在 task 方法中等待 IAsyncXxx^,项目其余部分保持不变

我们已了解可以如何 co_await task<void>。 还可以 co_await 返回 IAsyncXxx 的方法,无论这是项目中的方法还是异步 Windows API(例如我们在上一部分中协同等待的 StorageFolder.GetFileAsync)。

作为我们可以进行此类代码更改的一个示例,让我们来看一下 BasicReaderWriter::ReadDataAsync(你会发现它是在 BasicReaderWriter.cpp 中实现的)。

下面是原始 C++/CX 版本。

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
    )
{
    return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
    {
        return FileIO::ReadBufferAsync(file);
    }).then([=](IBuffer^ buffer)
    {
        auto fileData = ref new Platform::Array<byte>(buffer->Length);
        DataReader::FromBuffer(buffer)->ReadBytes(fileData);
        return fileData;
    });
}

以下代码列表显示我们可以 co_await 返回 IAsyncXxx^ 的 Windows API。 不仅如此,还可以 co_return 异步返回 BasicReaderWriter::ReadDataAsync 的值(在本例中为字节数组)。 第一步演示如何只进行这些更改;我们将在下一部分实际将 C++/CX 代码移植到 C++/WinRT。

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
)
{
    StorageFile^ file = co_await m_location->GetFileAsync(filename);
    IBuffer^ buffer = co_await FileIO::ReadBufferAsync(file);
    auto fileData = ref new Platform::Array<byte>(buffer->Length);
    DataReader::FromBuffer(buffer)->ReadBytes(fileData);
    co_return fileData;
}

同样,我们不需要更改我们更改的方法 的调用方 ,因为我们没有更改返回类型。

ReadDataAsync (大部分)移植到 C++/WinRT,使项目的其余部分保持不变

我们可以更进一步地将方法 移植 到 C++/WinRT,而无需更改项目的任何其他部分。

此方法对项目其余部分的唯一依赖项是 BasicReaderWriter::m_location 数据成员,它是 C++/CX StorageFolder^。 若要使该数据成员保持不变,并且要使参数类型和返回类型保持不变,只需执行几个转换-一个在方法的开头,一个在末尾。 为此,可以使用 from_cxto_cx 互操作帮助程序函数。

在将其实现主要移植到 C++/WinRT 之后,BasicReaderWriter::ReadDataAsync 如下所示。 这是 逐步移植的一个很好的示例。 这种方法已经到了这样一个阶段:我们不再把它看作 一种使用了一些 C++/WinRT 技术的 C++/CX 方法,而是把它看作 一种与 C++/CX 互操作的 C++/WinRT 方法

#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
#include <robuffer.h>
...
task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename)
{
    auto location_from_cx = from_cx<winrt::Windows::Storage::StorageFolder>(m_location);

    auto file = co_await location_from_cx.GetFileAsync(filename->Data());
    auto buffer = co_await winrt::Windows::Storage::FileIO::ReadBufferAsync(file);
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));

    co_return ref new Platform::Array<byte>(bytes, buffer.Length());
}

注释

在上面的 ReadDataAsync 中,我们构造并返回新的 C++/CX 数组。 当然,为了满足方法的返回类型(因此我们不必更改项目的其余部分),我们这样做。

你可能会在自己的项目中遇到其他示例,其中,移植后,你到达方法的末尾,你拥有的只是一个 C++/WinRT 对象。 若要 co_return 它,只需调用 to_cx 进行转换即可。 下一节将提供有关这方面的更多信息和一个示例。

winrt::IAsyncXxx<T> 转换为 task<T>

本部分介绍已将异步方法移植到 C++/WinRT 的情况(以便它返回 winrt::IAsyncXxx<T>),但仍有 C++/CX 代码调用该方法,就好像它仍在返回任务一样。

  • 一种情况是 T 是基元,不需要转换。
  • 另一种情况是 T 是Windows 运行时类型,在这种情况下,需要将该类型转换为 T^。

winrt::IAsyncXxx<T>(T 为基本类型)转换为 task<T>

本部分中的模式适用于异步返回基元值(我们将使用布尔值来说明)。 考虑这样一个示例,其中一个你已经移植到 C++/WinRT 的方法具有如下签名。

winrt::Windows::Foundation::IAsyncOperation<bool>
MyClass::GetBoolMemberFunctionAsync()
{
    bool value = ...
    co_return value;
}

可以像下面这样将对该方法的调用转换为任务。

task<bool> MyClass::RetrieveBoolTask()
{
    co_return co_await GetBoolMemberFunctionAsync();
}

或者这样。

task<bool> MyClass::RetrieveBoolTask()
{
    return concurrency::create_task(
        [this]() -> concurrency::task<bool> {
            auto result = co_await GetBoolMemberFunctionAsync();
            co_return result;
        });
}

请注意,lambda 函数 的任务 返回类型是显式的,因为编译器无法推断它。

我们还可以从任意任务链中调用该方法,如下所示。 同样,使用显式 Lambda 返回类型。

...
.then([this]() -> concurrency::task<bool> {
    co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
    ...
});
...

winrt::IAsyncXxx<T>(T 是 Windows 运行时 类型)转换为 task<T^>

本部分中的模式适用于异步返回Windows 运行时值(我们将使用 StorageFile 值来说明)。 考虑这样一个示例:你已移植到 C++/WinRT 的某个方法具有如下签名。

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
MyClass::GetStorageFileMemberFunctionAsync()
{
    co_return co_await winrt::Windows::Storage::StorageFile::GetFileFromPathAsync
    (L"MyFile.txt");
}

下面的代码清单演示了如何将对该方法的调用转换为任务。 请注意,我们需要调用 to_cx 互操作帮助程序函数,将返回的 C++/WinRT 对象转换为 C++/CX 句柄(也称为 hat)对象。

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    winrt::Windows::Storage::StorageFile storageFile =
        co_await GetStorageFileMemberFunctionAsync();
    co_return to_cx<Windows::Storage::StorageFile>(storageFile);
}

下面是一个更简洁的版本。

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    co_return to_cx<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}

你甚至可以选择将该模式包装为可重复使用的函数模板,并像通常返回任务那样 return 它。

template<typename ResultTypeCX, typename Awaitable>
concurrency::task<ResultTypeCX^> to_task(Awaitable awaitable)
{
    co_return to_cx<ResultTypeCX>(co_await awaitable);
}

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    return to_task<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}

如果你喜欢这个想法,你可能想要将 to_task 添加到 interop_helpers.h

让 create_async 环绕使用 co_return 的任务

无法直接 co_return IAsyncXxx^,但可以实现类似目的。 如果你有一个以协作方式返回值的任务,则可以将其包装在对 concurrency::create_async 的调用中。

这里举一个假设性的例子,因为我们无法从 Simple3DGameDX 中直接借用现成的示例。

Windows::Foundation::IAsyncOperation<bool>^ MyClass::RetrieveBoolAsync()
{
    return concurrency::create_async(
        [this]() -> concurrency::task<bool> {
            bool result = co_await GetBoolMemberFunctionAsync();
            co_return result;
        });
}

如你所见,可通过可以 co_await 的任何方法获得返回值。

concurrency::wait 移植到 co_await winrt::resume_after

有几处地方,Simple3DGameDX 使用 concurrency::wait 让线程暂停一小段时间。 下面是一个示例。

// GameConstants.h
namespace GameConstants
{
    ...
    static const int InitialLoadingDelay = 2000;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::CreateGameDeviceResourcesAsync(_In_ Simple3DGame^ game)
{
    std::vector<task<void>> tasks;
    ...
    tasks.push_back(create_task([]()
    {
        wait(GameConstants::InitialLoadingDelay);
    }));
    ...
}

concurrency::wait 的 C++/WinRT 版本是 winrt::resume_after 结构体。 我们可以在 PPL 任务中 co_await 该结构。 下面是一个代码示例。

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto InitialLoadingDelay = 2000ms;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::CreateGameDeviceResourcesAsync(_In_ Simple3DGame^ game)
{
    std::vector<task<void>> tasks;
    ...
    tasks.push_back(create_task([]() -> task<void>
    {
        co_await winrt::resume_after(GameConstants::InitialLoadingDelay);
    }));
    ...
}

请注意我们必须进行的另外两项更改。 我们已将 GameConstants::InitialLoadingDelay 的类型更改为 std::chrono::d uration,并显式生成 lambda 函数的返回类型,因为编译器不再能够推断出它。

task<void> 返回类型转换为 winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

在我们处理 Simple3DGameDX 的这一阶段,项目中所有调用 Simple3DGame::LoadLevelAsync 的地方都使用 co_await 来调用它。

这意味着,我们只需将该方法的返回类型从任务<void> 更改为 winrt::Windows::Foundation::IAsyncAction(使其余部分保持不变)。

winrt::Windows::Foundation::IAsyncAction Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    co_return co_await m_renderer->LoadLevelResourcesAsync();
}

现在应该相当机械地将该方法的其余部分及其依赖项(如 m_level等)移植到 C++/WinRT。

GameRenderer::LoadLevelResourcesAsync

下面是 GameRenderer::LoadLevelResourcesAsync 的原始 C++/CX 版本。

// GameConstants.h
namespace GameConstants
{
    ...
    static const int LevelLoadingDelay = 500;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;

    return create_task([this]()
    {
        wait(GameConstants::LevelLoadingDelay);
    });
}

Simple3DGame::LoadLevelAsync 是该项目中唯一调用 GameRenderer::LoadLevelResourcesAsync 的地方,并且它已经使用 co_await 来调用它。

因此,不再需要 GameRenderer::LoadLevelResourcesAsync 返回任务,而是可以返回 winrt::Windows::Foundation::IAsyncAction。 实现本身非常简单,可以完全移植到 C++/WinRT。 这涉及进行我们在将 concurrency::wait 移植到 co_await winrt::resume_after 中进行的相同更改。 并且对于项目的其余部分没有重大依赖关系需要担心。

因此,方法在完全移植到 C++/WinRT 后的外观如下。

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto LevelLoadingDelay = 500ms;
    ...
}

// GameRenderer.cpp
winrt::Windows::Foundation::IAsyncAction GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;
    co_return co_await winrt::resume_after(GameConstants::LevelLoadingDelay);
}

目标 - 完全将方法移植到 C++/WinRT

让我们最后通过一个最终目标示例来结束本次演练:将方法 BasicReaderWriter::ReadDataAsync 完整移植到 C++/WinRT。

上次我们看到这个方法时(在 ReadDataAsync(大部分)移植到 C++/WinRT,其余项目保持不变 一节中),它已经 基本上 移植到 C++/WinRT。 但它仍然返回 Platform::Array<byte>^ 的任务

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename)
{
    auto location_from_cx = from_cx<winrt::Windows::Storage::StorageFolder>(m_location);

    auto file = co_await location_from_cx.GetFileAsync(filename->Data());
    auto buffer = co_await winrt::Windows::Storage::FileIO::ReadBufferAsync(file);
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));

    co_return ref new Platform::Array<byte>(bytes, buffer.Length());
}

我们会将其更改为返回 IAsyncOperation,而不是返回任务。 并且,我们将不再通过该 IAsyncOperation 返回字节数组,而是返回一个 C++/WinRT IBuffer 对象。 这还需要对呼叫站点上的代码进行轻微更改,正如我们看到的。

下面展示了将该方法的实现、参数和 m_location 数据成员迁移为使用 C++/WinRT 语法和对象后,该方法的样子。

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::Streams::IBuffer>
BasicReaderWriter::ReadDataAsync(
    _In_ winrt::hstring const& filename)
{
    StorageFile file{ co_await m_location.GetFileAsync(filename) };
    co_return co_await FileIO::ReadBufferAsync(file);
}

winrt::array_view<byte> BasicLoader::GetBufferView(
    winrt::Windows::Storage::Streams::IBuffer const& buffer)
{
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));
    return { bytes, bytes + buffer.Length() };
}

如你所见, BasicReaderWriter::ReadDataAsync 本身要简单得多,因为我们已将自己的方法分解为从缓冲区检索字节的同步逻辑。

但现在我们需要将调用点从 C++/CX 中的这种结构迁移出来。

task<void> BasicLoader::LoadTextureAsync(...)
{
    return m_basicReaderWriter->ReadDataAsync(filename).then(
        [=](const Platform::Array<byte>^ textureData)
    {
        CreateTexture(...);
    });
}

C++/WinRT 中的这种模式。

winrt::Windows::Foundation::IAsyncAction BasicLoader::LoadTextureAsync(...)
{
    auto textureBuffer = co_await m_basicReaderWriter.ReadDataAsync(filename);
    auto textureData = GetBufferView(textureBuffer);
    CreateTexture(...);
}

重要 API