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 运行时命名空间包含四种类型的异步操作对象。
- IAsyncAction,
- IAsyncActionWithProgress<TProgress>,
- IAsyncOperation<TResult>,和
- IAsyncOperationWithProgress<TResult、TProgress>。
在本主题中,当我们使用方便的简写 IAsyncXxx 时,可能是统称这些类型;也可能是指四种类型中的某一种,而无需明确说明具体是哪一种。
C++/CX 异步
异步 C++/CX 代码使用 并行模式库 (PPL) 任务。 PPL 任务由 并发::task 类表示。
通常,异步 C++/CX 方法通过将 lambda 函数与 concurrency::create_task 和 concurrency::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::IAsyncXxx 或 winrt::fire_and_forget。 与使用 return 关键字返回异步对象相反,协同例程使用 co_return 关键字协作返回调用方实际想要的值(可能是文件、字节数组或布尔值)。
如果某个方法包含至少一个 co_await 语句(或至少一个 co_return 或 co_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_cx 和 to_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_await 在 task<void> 方法中等待 task<void> 方法。 |
在task<void>方法中等待task<void> |
使用 co_await 在 task<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_await 和 this 指针的延后讨论)——不过现在,这段代码可以正常工作。
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_await 和 this 指针的延后讨论
当我们更改 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_cx 和 to_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::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
- IAsyncAction
- IAsyncActionWithProgress<TProgress>
- IAsyncOperation<TResult>
- IAsyncOperationWithProgress<TResult, TProgress>
- implements::get_strong
- concurrency::create_async
- concurrency::create_task
- concurrency::task
- concurrency::task::then
- concurrency::wait
- winrt::fire_and_forget
- winrt::make_self