异步,以及 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++ 语言投影分别以不同方式构建于其之上的情况。

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

  • 通常想要等待异步工作完成,然后再执行其他操作。 返回异步操作对象的方法是可等待的方法。
  • 但有时你不希望或需要等待异步完成的工作。 在这种情况下,异步方法 返回异步操作对象的效率更高。 像那样的异步方法——即你不会等待其完成的方法——被称为 发后不理 方法。

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> 方法。 在即发即弃方法中 await 任务<void>
使用 co_awaittask<void> 方法中等待 task<void> 方法。 task<void>方法中等待task<void>
使用 co_awaittask<T> 方法中等待 task<void> 方法。 任务<T> 方法中等待任务<void>
使用 co_await 等待 IAsyncXxx^ 方法。 task方法中等待IAsyncXxx^,而项目的其余部分保持不变
co_returntask<void> 方法中使用。 task<void> 方法中等待 task<void>
任务<T> 方法内使用 co_return task方法中等待 IAsyncXxx^,而使项目的其余部分保持不变
围绕使用的任务包装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

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

等待 任务<void> 方法,使项目的其余部分保持不变

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

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

注释

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

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

Important

在下面的示例中,当你看到方法的实现发生更改时,请记住,我们不需要更改我们更改的方法的 调用方 。 这些更改仅限于局部,不会在整个项目中级联传播。

在 fire-and-forget 方法中等待 task<void>

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

查看项目依赖关系图的根部,查找包含 create_taskvoid 的方法和/或仅调用 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 中的内容更有趣,因为它更深入地嵌套异步代码,包括 no-op 任务。 下面是该方法中异步部分的概述(省略号表示其余较乏味的同步代码)。

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,那就直接删除它即可。 完成后,no-op 任务将消失,方法的异步部分的轮廓如下所示。 同样,没那么有趣的同步代码也被省略了。

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 中所有调用后不等待结果的方法——事实上,所有异步代码——都已转换为协程。 如果你愿意,也许可以在其他类中查找采用 fire-and-forget 模式的方法,并进行类似的修改。

关于 co_awaitthis 指针的延后讨论

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

这适用于到目前为止我们修改过的所有方法;而且它适用于所有协程,而不只是即发即弃的协程。 在方法中引入一个co_await会引入一个挂起点。 因此,我们必须谨慎 使用此 指针,当然,每次访问类成员时,我们都会在挂起点 使用该指针。

简言之,解决方案是调用 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一个任务<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。

任务方法中等待 IAsyncXxx^ ,使项目的其余部分保持不变

我们已经了解了如何 co_await任务<无效>。 你还可以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。 这需要在 Port concurrency::waitco_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