Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Подсказка
Хотя мы рекомендуем прочитать этот раздел с самого начала, вы можете перейти непосредственно к сводке методов взаимодействия в разделе "Обзор переноса C++/CX async в C++/WinRT ".
Это сложная тема, связанная с постепенным переходом на C++/WinRT с C++/CX. Этот раздел продолжает тему, начатую в разделе Взаимодействие между C++/WinRT и C++/CX.
Если размер или сложность базы кода позволяет постепенно переносить проект, вам потребуется процесс переноса, в течение которого код C++/CX и C++/WinRT существует параллельно в том же проекте. Если у вас есть асинхронный код, то по мере постепенного переноса исходного кода вам может потребоваться, чтобы в проекте одновременно сосуществовали цепочки задач библиотеки Parallel Patterns Library (PPL) и корутины. В этом разделе рассматриваются методы взаимодействия между асинхронным кодом C++/CX и асинхронным кодом C++/WinRT. Эти методы можно использовать по отдельности или вместе. Эти методы позволяют вносить постепенные, контролируемые локальные изменения в процессе переноса всего проекта, не допуская, чтобы каждое изменение вызывало неконтролируемую цепную реакцию во всём проекте.
Перед чтением этой статьи рекомендуется читать взаимодействие между C++/WinRT и C++/CX. В этом разделе показано, как подготовить проект к постепенному переносу. Он также представляет две вспомогательные функции, которые можно использовать для преобразования объекта C++/CX в объект C++/WinRT (и наоборот). Этот раздел об асинхронности основан на этих сведениях и использует эти вспомогательные функции.
Note
Существуют некоторые ограничения для постепенного переноса из C++/CX в C++/WinRT. Если у вас есть проект компонента среда выполнения Windows, перенос постепенно невозможен, и вам потребуется перенести проект в один проход. И для проекта XAML в любой момент все типы страниц XAML должны быть либо C++/WinRT, либо C++/CX. Дополнительные сведения см. в разделе "Перемещение на C++/WinRT" из C++/CX.
Почему целый раздел посвящён асинхронному взаимодействию кода
Переход с 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#, если вы хотите.
Немного об асинхронном программировании
Чтобы у нас было общее понимание понятий и терминологии асинхронного программирования, давайте кратко обрисуем общую картину асинхронного программирования в среда выполнения Windows в целом, а также того, как поверх неё, каждая по-своему, построены две языковые проекции C++.
Проект имеет методы, которые работают асинхронно, и существует два основных типа.
- Обычно требуется ждать завершения асинхронной работы, прежде чем выполнять что-то другое. Метод, возвращающий объект асинхронной операции, — это метод, результат которого можно ожидать.
- Но иногда вы не хотите или не должны ждать завершения работы, выполняемой асинхронно. В этом случае асинхронному методу эффективнее не возвращать объект асинхронной операции. Такой асинхронный метод — метод, завершения которого вы не ожидаете, — называют методом fire-and-forget.
асинхронные объекты среда выполнения Windows (IAsyncXxx)
Пространство имен Windows::Foundation среда выполнения Windows содержит четыре типа объекта асинхронной операции.
- IAsyncAction,
- IAsyncActionWithProgress<TProgress,>
- IAsyncOperation<TResult>, и
- IAsyncOperationWithProgress<TResult, TProgress>.
В этом разделе, когда мы используем удобное сокращение IAsyncXxx, мы имеем в виду либо все эти типы вместе, либо один из четырех типов без необходимости уточнять, какой именно.
C++/CX async
Асинхронный код C++/CX использует задачи библиотеки параллельных шаблонов (PPL). Задача PPL представлена классом concurrency::task.
Как правило, асинхронный метод C++/CX связывает задачи PPL в цепочку с помощью лямбда-функций, используя concurrency::create_task и concurrency::task::then. Каждая лямбда-функция возвращает задачу, которая, когда она завершается, создает значение, которое затем передается в лямбда-функцию продолжения задачи.
В качестве альтернативы, вместо вызова create_task для создания задачи, асинхронный метод C++/CX может вызвать concurrency::create_async для создания IAsyncXxx^.
Поэтому возвращаемый тип асинхронного метода C++/CX может быть задачей PPL или IAsyncXxx^.
В любом случае сам метод использует return ключевое слово для возврата асинхронного объекта, который, когда он завершится, создает значение, которое вызывающий объект фактически хочет (возможно, файл, массив байтов или логическое значение).
Note
Если асинхронный метод 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 (в отличие от IAsyncXxx^ в C++/CX).
Возвращаемый тип корутины C++/WinRT — winrt::IAsyncXxx илиwinrt::fire_and_forget. Вместо того чтобы использовать ключевое слово return для возврата асинхронного объекта, корутина использует ключевое слово co_return, чтобы совместно вернуть значение, которое на самом деле нужно вызывающей стороне (например, файл, массив байтов или логическое значение).
Если метод содержит хотя бы один оператор co_await (или хотя бы один co_return или co_yield), то этот метод является корутиной уже поэтому.
Дополнительные сведения и примеры кода см. в разделе Параллелизм и асинхронные операции с C++/WinRT.
Пример игры Direct3D (Simple3DGameDX)
В этом разделе содержатся пошаговые руководства по нескольким конкретным методам программирования, которые иллюстрируют, как постепенно переносить асинхронный код. Для работы в качестве примера мы будем использовать версию C++/CX примера игры Direct3D (которая называется Simple3DGameDX). Мы рассмотрим некоторые примеры того, как можно взять исходный исходный код C++/CX в этом проекте и постепенно перенести асинхронный код в C++/WinRT.
- Скачайте ZIP-файл из приведенной выше ссылки и распакуйте его.
- Откройте проект C++/CX (он находится в папке с именем
cpp) в Visual Studio. - Затем необходимо добавить в проект поддержку C++/WinRT. Описанные ниже действия описаны в разделе "Создание проекта C++/CX" и добавление поддержки C++/WinRT. В этом разделе шаг о добавлении файла заголовка
interop_helpers.hв проект особенно важен, так как мы будем зависеть от этих вспомогательных функций в этом разделе. - Наконец, добавьте
#include <pplawait.h>вpch.h. Это дает поддержку соподдержки для PPL (дополнительные сведения об этой поддержке см. в следующем разделе).
Еще не создавайте, в противном случае вы получите ошибки о неоднозначности байтов . Вот как устранить это.
- Откройте
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::IAsyncXxxx . И мы также изменим любой 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 от него? |
|---|---|---|
| Метод возвращает задачу<void> | Yes | Yes |
| Метод возвращает задачу<T> | No | Yes |
| Метод возвращает IAsyncXxx^ | Yes | Нет. Но вы оборачиваете create_async вокруг задачи, которая использует co_return. |
| Метод возвращает winrt::IAsyncXxx | Yes | Yes |
Используйте следующую таблицу, чтобы сразу перейти к разделу этой темы, в котором описан интересующий вас способ взаимодействия, или просто продолжайте читать дальше.
| Метод асинхронного взаимодействия | Раздел в этой теме |
|---|---|
Используйте co_await для ожидания метода task<void> внутри метода fire-and-forget или конструктора. |
Ожидание пустоты< задачи> в методе fire-and-forget |
Используйте co_await для ожидания метода task<void> из метода task<void>. |
Ожидать task<void> в методе task<void> |
Используйте co_await для ожидания метода task<void> внутри метода task<T>. |
Ожидание задачи<void> в методе задачи<T> |
Используется co_await для ожидания метода IAsyncXxx^. |
Ожидать IAsyncXxx^ в методе task, не изменяя остальной проект |
Используйте co_return в методе task<void> . |
Ожидание пустоты< задачи> в методе void задачи<> |
Используйте co_return в методе task<T> . |
Ожидать IAsyncXxx^ в методе task, не изменяя остальную часть проекта |
Оберните задачу, использующую co_return, в create_async. |
Оберните 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();
}
Это важно
Даже при наличии этих отличных возможностей взаимодействия поэтапный перенос зависит от выбора изменений, которые можно вносить точечно, не затрагивая остальной проект. Мы хотим избежать того, чтобы потянуть за какой-нибудь случайный свободный конец и тем самым распустить всю структуру проекта. Для этого мы должны делать вещи в определенном порядке. Далее мы подробно рассмотрим некоторые примеры внесения таких изменений, связанных с асинхронным переносом и взаимодействием.
Ожидать метод task<void>, оставляя остальной проект без изменений
Метод, возвращающий пустоту< задачи>, выполняет работу асинхронно, и возвращает объект асинхронной операции, но в конечном счете не создает значение. Мы можем co_await использовать такой метод.
Поэтому лучше начать перенос асинхронного кода постепенно— найти места, где вы вызываете такие методы. Эти места будут включать создание и /или возврат задачи. Они также могут включать в себя тип цепочки задач, в которой значение не передается из каждой задачи в продолжение. В таких местах можно просто заменить асинхронный код инструкциями co_await, как мы увидим.
Note
По мере дальнейшего рассмотрения этой темы вы увидите, в чем преимущества этой стратегии. Как только некоторый метод task<void> начинает вызываться исключительно через co_await, вы можете перенести этот метод на C++/WinRT и сделать так, чтобы он возвращал winrt::IAsyncXxx.
Давайте рассмотрим некоторые примеры. Откройте проект Simple3DGameDX (см. пример игры Direct3D).
Это важно
В приведенных ниже примерах, когда вы будете видеть, как меняется реализация методов, имейте в виду, что нам не нужно менять места вызова методов, реализацию которых мы меняем. Эти изменения локализованы, и они не каскадируются через проект.
Ожидать task<void> в методе «запустил и забыл»
Начнем с ожиданий пустоты< задачи> в методах fire-and-forget, так как это самый простой случай. Это методы, которые выполняются асинхронно, но вызывающий код не ждёт завершения этой работы. Вы просто вызываете метод и больше о нём не думаете, хотя он выполняется асинхронно.
Ищите в корне графа зависимостей проекта void методы, содержащие create_task, и/или цепочки задач, в которых вызываются только методы 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 является асинхронным, но он не возвращает значение. Таким образом, от задачи к продолжению не передается никакое значение.
Мы можем внести одно и то же изменение в код в этих двух местах. Код объясняется после перечисления ниже. Мы могли бы обсудить здесь о безопасном способе доступа к этому указателю в корутине члена класса. Но давайте отложим это для последующего раздела (отложенное обсуждение co_await и этот указатель) — на данный момент этот код работает.
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. Это метод типа fire-and-forget, поэтому мы изменили его так, чтобы он возвращал winrt::fire_and_forget.
Вам также потребуется изменить GameMain.h. Также измените тип возвращаемого значения GameMain::Update с void на winrt::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::OnDeviceRestored с void на winrt::fire_and_forget в GameMain.h и .cpp. Кроме того, необходимо открыть DeviceResources.h и внести то же изменение в тип возвращаемого значения IDeviceNotify::OnDeviceRestored.
Чтобы перенести асинхронный код, удалите все вызовы create_task и then и их фигурные скобки, а также упростите метод, преобразовав его в плоскую последовательность операторов.
Измените любой объект 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, поэтому мы переместим асинхронный код в новый метод fire-and-forget GameMain::ConstructInBackground, преобразуем код в последовательность операторов 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();
...
}
...
}
Теперь все методы fire-and-forget (на самом деле, весь асинхронный код) в GameMain были преобразованы в корутины. Если захотите, можете поискать методы типа fire-and-forget в других классах и внести аналогичные изменения.
Отложенное обсуждение co_await и указателя this
Когда мы вносим изменения в GameMain::Update, я отложил обсуждение этого указателя. Давайте поговорим здесь.
Это относится ко всем методам, которые мы изменили к настоящему моменту; и ко всем корутинам, а не только к тем, которые запускаются без ожидания завершения. Добавление co_await в метод приводит к появлению точки приостановки. И из-за этого мы должны быть осторожны с указателем this, поскольку мы, конечно, используем его после точки приостановки каждый раз, когда обращаемся к члену класса.
Если коротко, решение состоит в том, чтобы вызвать implements::get_strong. Но для полного обсуждения проблемы и решения см. безопасный доступ к этому указателю в корутине члена класса.
Вы можете вызывать implements::get_strong только в классе, производном от winrt::implements.
Унаследуйте GameMain от winrt::implements
Первое изменение, в который мы должны внести, заключается в 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сделать задачу<пустой>, и мы можем 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();
}
Это не выглядит как глубокое изменение. Но теперь, когда мы вызываем GameRenderer::LoadLevelResourcesAsync через co_await, мы можем адаптировать этот метод так, чтобы он возвращал winrt::IAsyncXxx вместо task. Мы сделаем это позже в разделе
Ожидание task<void> в методе task<T>
Хотя в Simple3DGameDX нельзя найти подходящих примеров, мы можем придумать гипотетический пример, просто чтобы показать общий принцип.
Первая строка в приведенном ниже примере кода демонстрирует простую co_awaitпустоту< задачи>. Затем, чтобы соответствовать возвращаемому типу task<T>, необходимо асинхронно возвращать StorageFile^. Для этого мы co_await API среды выполнения Windows и 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 в этом примере.
Используйте await для IAsyncXxx^ в методе task, не изменяя остальную часть проекта
Мы видели, как можно co_awaitвыполнять задачу<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 Windows API, возвращающие IAsyncXx^. Кроме того, мы можем также 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.
Ниже показано, как выглядит BasicReaderWriter::ReadDataAsync после переноса его реализации преимущественно на C++/WinRT. Это хороший пример постепенного переноса. И этот метод находится на этапе, где мы можем отойти от его мышления как метод C++/CX, который использует некоторые методы C++/WinRT, и увидеть его как метод C++/WinRT, который взаимодействует с C++/CX.
#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());
}
Note
В 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;
});
}
Обратите внимание, что тип возврата задачи лямбда-функции является явным, так как компилятор не может его вывести.
Мы также могли бы вызвать метод внутри произвольной цепочки задач, например, так. Снова, с явно указанным типом возвращаемого значения лямбда-выражения.
...
.then([this]() -> concurrency::task<bool> {
co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
...
});
...
Преобразовать winrt::IAsyncXxx<T> (T — тип среды выполнения Windows (среда выполнения 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. Это можно co_await сделать внутри задачи PPL. Ниже приведен пример кода.
// 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, и мы сделали тип возвращаемой функции лямбда-функции явным образом, так как компилятор больше не может вывести его.
Перенести тип возвращаемого значения task<void> в winrt::IAsyncXxx
Simple3DGame::LoadLevelAsync
На этом этапе работы с Simple3DGameDX во всех местах проекта, где вызывается Simple3DGame::LoadLevelAsync, для этого вызова используется co_await.
Это означает, что мы можем просто изменить тип возвращаемого значения этого метода с task<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
Вот исходная версия C++/CX для GameRenderer::LoadLevelResourcesAsync.
// 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.
В последний раз мы рассмотрели этот метод (в разделе Port 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
- параллелизм::task
- параллелизм::task::then
- параллелизм::wait
- winrt::fire_and_forget
- winrt::make_self
Связанные темы
Windows developer