Асинхронное выполнение задач и взаимодействие между C++/WinRT и C++/CX

Совет

Хотя мы рекомендуем прочитать этот раздел с самого начала, вы можете сразу перейти к краткому описанию методов взаимодействия в разделе Общие сведения о переносе асинхронной операции 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++/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 Runtime, а также то, из каких слоев состоит каждая из двух проекций языка С++ в этом контексте.

В проекте есть методы двух основных типов, работающие асинхронно.

  • Зачастую требуется дождаться завершения асинхронной операции, прежде чем выполнять что-то еще. Метод, возвращающий объект асинхронной операции, — это метод, который можно ожидать.
  • Однако иногда в ожидании завершения работы в асинхронном режиме нет необходимости. В этом случае для асинхронного метода целесообразно не возвращать объект асинхронной операции. Асинхронный метод без ожидания выполнения называется методом Выполнил и забыл.

Асинхронные объекты среды выполнения Windows(IAsyncXxx)

Пространство имен среды выполнения Windows Windows::Foundation содержит четыре типа объектов асинхронной операции.

В этом разделе, когда мы используем удобное сокращение IAsyncXxx, мы имеем в виду либо эти типы вместе, либо мы говорим об одном из четырех типов без необходимости указывать какой именно.

Асинхронный C++/CX

В асинхронном коде 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 для возвращения асинхронного объекта, который, по завершении работы, создает значение, необходимое самому вызывающему объекту (возможно, файл, массив байтов или логическое значение).

Примечание.

Если асинхронный метод C++/CX возвращает IAsyncXxx^, то TResult (при наличии) ограничивается типом среды выполнения Windows. Логическое значение, например, является типом среды выполнения Windows; а проектируемый тип C++/CX (например, Platform::Array<byte>^) — нет.

Асинхронный C++/WinRT

C++/WinRT интегрирует сопрограммы C++ в модель программирования. Сопрограммы и оператор co_await обеспечивают естественный способ совместного ожидания результата.

Каждый из этих типов асинхронной операции IAsyncXxx проецируется в соответствующий тип в пространстве имен C++/WinRT winrt::Windows::Foundation. Давайте будем называть их 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. Это можно сделать с помощью функций поиска и замены в файле.
  • Квалифицируйте vector как std::vector и string как 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) для возврата задачи PPL из сопрограммы можно также использовать co_return. Кроме того, можно co_await как задачи, так и IAsyncXxx. Однако вы не можете использовать co_return для возврата IAsyncXxx^. В следующей таблице описывается поддержка взаимодействия между различными асинхронными методами при использовании pplawait.h.

Метод Поддержка co_await Поддержка co_return
Метод возвращает task<void> Да Да
Метод возвращает task<T> No Да
Метод возвращает IAsyncXxx^ Да Нет. Но вы создаете программу-оболочку create_async для задачи, которая использует co_return.
Метод возвращает winrt::IAsyncXxx Да Да

Используйте следующую таблицу, чтобы перейти непосредственно к подразделу, в котором описывается интересующий вас метод взаимодействия, или просто продолжайте чтение отсюда.

Метод асинхронного взаимодействия Раздел в этом разделе
Используйте co_await, чтобы ожидать метод task<void> в методе "Выполнил и забыл" или в конструкторе. Ожидание task<void> в методе "Выполнил и забыл"
Используйте co_await, чтобы ожидать метод task<void> в методе task<void>. Ожидание task<void> в методе task<void>
Используйте co_await, чтобы ожидать метод task<void> в методе task<T>. Ожидание task<void> в методе task<T>
Используйте co_await для ожидания метода IAsyncXxx^. Ожидание IAsyncXxx^ в методе task, оставляя остальную часть проекта без изменений
Используйте co_return в методе task<void>. Ожидание task<void> в методе task<void>
Используйте co_return в методе task<T>. Ожидание IAsyncXxx^ в методе task, оставляя остальную часть проекта без изменений
Создайте программу-оболочку 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();
}

Важно!

Даже при использовании этих замечательных возможностей взаимодействия постепенный перенос зависит от выбора изменений, которые мы можем внести оперативным путем и которые не влияют на остальную часть проекта. Мы хотим избежать затягивания решения произвольной нерешенной проблемы и тем самым разобраться в структуре всего проекта. Для этого необходимо выполнить определенные действия в определенном порядке. Далее мы подробнее рассмотрим несколько примеров применения таких изменений, связанных с переносом асинхронного кода или взаимодействием с ним.

Ожидание метода task<void>оставляя остальную часть проекта без изменений

Метод, возвращающий task<void>, выполняет работу асинхронно и возвращает объект асинхронной операции, но в конечном итоге не создает значение. Мы можем co_await метод, подобный этому.

Поэтому лучше начать перенос асинхронного кода постепенно, чтобы найти места вызова таких методов. В этих местах будет выполняться создание и (или) возвращение задачи. В них также может выполняться цепочка задач, в которой задача не передает какое-либо значение следующей задаче в цепочке. В таких местах можно просто заменить асинхронный код на инструкции co_await, как увидим далее.

Примечание.

По мере изложения материала раздела вы увидите преимущества этой стратегии. Реализовав монопольный вызов метода task<void> посредством co_await вы можете свободно перенести этот метод в C++/WinRT, чтобы он возвращал winrt::IAsyncXxx.

Рассмотрим несколько примеров. Откройте проект Simple3DGameDX (см. Пример игры Direct3D).

Важно!

В последующих примерах, увидев реализации изменяемых методов, помните, что нет необходимости изменять вызывающие объекты методов, которые мы изменяем. Эти изменения локализованы и не размещаются каскадно в проекте.

Ожидание task<void> в методе "Выполнил и забыл"

Начнем с ожидания task<void> в методе "Выполнил и забыл", поскольку это самый простой вариант. Хотя это методы, которые работают асинхронно, вызывающий объект метода не ждет завершения этой работы. Вы просто вызываете метод и забываете о нем, несмотря на то, что он завершается асинхронно.

Найдите корень графа зависимостей вашего проекта для методов 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 и указателе 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 с void на winrt::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::OnDeviceRestored с void на winrt::fire_and_forget в GameMain.h и .cpp. Также необходимо открыть DeviceResources.h и внести то же изменение в тип возвращаемого значения IDeviceNotify::OnDeviceRestored.

Чтобы перенести асинхронный код, удалите все вызовы create_task и then и их фигурные скобки, а также упростите метод, изменив его в плоский ряд инструкций.

Измените любое значение 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, выполним сведения кода в инструкции 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_await и указателе this

При внесении изменений в GameMain::Update я отложил обсуждение об указателе this. Давайте рассмотрим этот вопрос здесь.

Это относится ко всем методам, которые мы изменили до сих пор, а также ко всем сопрограммам, а не только к сопрограммам метода "Выполнил и забыл". Введение co_await в метод приводит к точке приостановки. Поэтому мы должны быть осторожны с указателем this, который, конечно же, мы используем после точки приостановки при каждом обращении к члену класса.

Вкратце, решение заключается в вызове implements::get_strong. Полное обсуждение вопроса и его решение см. в разделе Безопасный доступ к указателю this в сопрограмме членов класса.

Метод 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

Ниже показано, как вызвать get_strong в начале сопрограммы, чтобы гарантировать, что строгая ссылка сохранится до завершения сопрограммы, для GameMain::Update и для любого из других методов, добавленных в co_await.

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

Это не похоже на существенное изменение. Но теперь, когда мы вызываем метод GameRenderer::LoadLevelResourcesAsync посредством co_await, мы можем свободно перенести его, чтобы он возвращал winrt::IAsyncXxx, а не task. Мы сделаем это позже, в разделе Перенос возвращаемого типа task<void> в winrt::IAsyncXxx.

Ожидание task<void> в методе task<T>

Хотя подходящих примеров в Simple3DGameDX нет, мы можем придумывать гипотетический пример, просто чтобы показать шаблон.

В первой строке приведенного ниже примера кода демонстрируется простое совместное ожидание co_awaittask<void>. Затем, чтобы обеспечить тип возвращаемого значения 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.

Ожидание IAsyncXxx^ в методе task, оставляя остальную часть проекта без изменений

Мы рассмотрели, как можно co_awaittask<void>. Можно также выполнить co_await для метода, который возвращает IAsyncXxx, независимо от того, является ли он методом в проекте или асинхронным API Windows (например, 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 для интерфейсов API Windows, которые возвращают IAsyncXxx^. И не только это, можно выполнить 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());
}

Примечание.

Ранее в ReadDataAsync мы создали и вернули новый массив C++/CX. И, конечно, мы сделали так, чтобы он соответствовал типу возвращаемого значения метода (так что нам не пришлось изменять остальную часть проекта).

Вы можете полагаться на другие примеры в собственном проекте, где, после переноса, вы достигли конца метода и все, что у вас есть, — это объект C++/WinRT. Чтобы co_return его, просто вызовите to_cx, чтобы преобразовать. Подробнее об этом вы узнаете в следующем разделе, в котором также приведен пример.

Преобразуйте winrt::IAsyncXxx<T> в task<T>

В этом разделе рассматривается ситуация, когда асинхронный метод был перенесен в C++ /WinRT (чтобы он возвращал winrt::IAsyncXxx<T>), но остался код C++/CX, вызывающий этот метод, как если бы он по-прежнему возвращал task.

  • В одном случае 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, как показано ниже.

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

Обратите внимание на то, что тип возвращаемого значения task лямбда-функции является явным, так как компилятор не может его вывести.

Можно также вызвать метод из произвольной цепочки задач, как показано ниже. Опять же, с явно указанным типом возвращаемого значения лямбда-функции.

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

В следующем листинге показано, как преобразовать вызов этого метода в task. Обратите внимание на то, что нам нужно вызвать вспомогательную функцию взаимодействия 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 для него так же, как и при обычном возвращении task.

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_returnIAsyncXxx^ напрямую, но можете сделать что-то подобное. Если имеется задача, которая совместно возвращает значение, можно перенести ее в вызов 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);
    }));
    ...
}

Версия conccurrency::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::duration и сделали тип возвращаемого значения лямбда-функции явным, так как компилятор больше не сможет его вывести.

Перенос возвращаемого типа 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 больше не нужно возвращать task, вместо этого он может возвращать 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