Assincronia e interoperabilidade entre C++/WinRT e C++/CX

Tip

Embora recomendemos que você leia este tópico do início, você pode ir diretamente para um resumo das técnicas de interoperabilidade na seção Visão geral da migração de código assíncrono de C++/CX para C++/WinRT.

Este é um tópico avançado sobre a migração gradual de C++/CX para C++/WinRT. Este tópico continua de onde o tópico Interoperabilidade entre C++/WinRT e C++/CX parou.

Se o tamanho ou a complexidade da base de código for necessário para portar seu projeto gradualmente, você precisará de um processo de portabilidade no qual, por um tempo, o código C++/CX e C++/WinRT exista lado a lado no mesmo projeto. Se você tiver código assíncrono, talvez seja necessário ter cadeias de tarefas PPL (Biblioteca de Padrões Paralelos) e corrotinas coexistindo em seu projeto conforme você porta seu código-fonte gradualmente. Este tópico se concentra em técnicas para interoperar entre código C++/CX assíncrono e código C++/WinRT assíncrono. Você pode usar essas técnicas individualmente ou em conjunto. As técnicas permitem que você faça alterações graduais, controladas e locais ao longo do caminho da portabilidade de todo o seu projeto, sem que cada alteração se propague em cascata sem controle por todo o projeto.

Antes de ler este tópico, é uma boa ideia ler Interoperabilidade entre C++/WinRT e C++/CX. Esse tópico mostra como preparar seu projeto para a portabilidade gradual. Ele também apresenta duas funções auxiliares que você pode usar para converter um objeto C++/CX em um objeto C++/WinRT (e vice-versa). Este tópico sobre assíncrona se baseia nessas informações e usa essas funções auxiliares.

Note

Há algumas limitações para a portabilidade gradualmente de C++/CX para C++/WinRT. Se você tiver um projeto de componente Windows Runtime, a portabilidade gradualmente não será possível e você precisará portar o projeto de uma só vez. E, para um projeto XAML, em um dado momento os tipos de página XAML devem ser ou todos C++/WinRT ou todos C++/CX. Para obter mais informações, consulte o tópico Migrar de C++/CX para C++/WinRT.

O motivo pelo qual um tópico inteiro é dedicado à interoperabilidade de código assíncrono

A portabilidade de C++/CX para C++/WinRT é geralmente simples, com a única exceção da migração de tarefas PPL (Biblioteca de Padrões Paralelos) para corrotinas. Os modelos são diferentes. Não há um mapeamento um a um natural de tarefas PPL para corrotinas e não há uma forma simples (que funcione para todos os casos) de portar mecanicamente o código.

A boa notícia é que a conversão de tarefas em corrotinas resulta em simplificações significativas. E as equipes de desenvolvimento relatam rotineiramente que, depois de superar o obstáculo de portar seu código assíncrono, o restante do trabalho de migração torna-se em grande parte mecânico.

Muitas vezes, um algoritmo era originalmente escrito para atender a APIs síncronas. E então isso foi traduzido em tarefas e continuações explícitas , o resultado muitas vezes sendo uma ofuscação inadvertida da lógica subjacente. Por exemplo, os loops se tornam recursão; branches if-else são convertidos em uma árvore aninhada (uma cadeia) de tarefas; as variáveis compartilhadas se tornam shared_ptr. Para desmontar a estrutura frequentemente pouco natural do código-fonte em PPL, recomendamos que você primeiro dê um passo para trás e compreenda a intenção do código original (isto é, identifique a versão síncrona original). E, em seguida, insira co_await (aguarde cooperativamente) nos locais apropriados.

Por esse motivo, se você tiver uma versão C# (em vez de C++/CX) do código assíncrono do qual iniciar sua porta, isso poderá lhe dar um tempo mais fácil e uma porta mais limpa. O código C# usa await. Portanto, o código C# já segue essencialmente uma filosofia de começar com uma versão síncrona e, em seguida, inserir await nos locais apropriados.

Se você não tiver uma versão em C# do seu projeto, poderá usar as técnicas descritas neste tópico. E depois de portar para C++/WinRT, a estrutura do código assíncrono será mais fácil de portar para C#, caso deseje.

Noções básicas de programação assíncrona

Para que tenhamos um referencial comum para os conceitos e a terminologia da programação assíncrona, vamos contextualizar brevemente a programação assíncrona do Windows Runtime em geral e também como as duas projeções da linguagem C++ são, cada uma à sua maneira, construídas sobre essa base.

Seu projeto tem métodos que funcionam de forma assíncrona e há dois tipos principais.

  • É comum querer aguardar a conclusão do trabalho assíncrono antes de fazer outra coisa. Um método que retorna um objeto de operação assíncrono é aquele que você pode aguardar.
  • Mas, às vezes, você não quer ou precisa aguardar a conclusão do trabalho feito de forma assíncrona. Nesse caso, é mais eficiente para o método assíncrono não retornar um objeto de operação assíncrono. Um método assíncrono como, aquele que você não aguarda, é conhecido como um método fire-and-forget.

objetos assíncronos do Windows Runtime (IAsyncXxx)

O namespace Windows::Foundation Windows Runtime contém quatro tipos de objeto de operação assíncrona.

Neste tópico, quando usamos a abreviação conveniente IAsyncXxx, estamos nos referindo a esses tipos coletivamente ou a um dos quatro tipos, sem precisar especificar qual.

C++/CX assíncrono

O código C++/CX assíncrono usa tarefas ppl (Biblioteca de Padrões Paralelos ). Uma tarefa PPL é representada pela classe concurrency::task .

Normalmente, um método assíncrono em C++/CX encadeia tarefas PPL usando funções lambda com concurrency::create_task e concurrency::task::then. Cada função lambda retorna uma tarefa que, quando concluída, produz um valor que é passado para o lambda da continuação da tarefa.

Como alternativa, em vez de chamar create_task para criar uma tarefa, um método assíncrono em C++/CX pode chamar concurrency::create_async para criar um IAsyncXxx^.

Portanto, o tipo de retorno de um método C++/CX assíncrono pode ser uma tarefa PPL ou um IAsyncXxx^.

Em ambos os casos, o próprio método usa a return palavra-chave para retornar um objeto assíncrono que, quando concluído, produz o valor que o chamador realmente deseja (talvez um arquivo, uma matriz de bytes ou um booliano).

Note

Se um método C++/CX assíncrono retornar um IAsyncXxx^, o TResult (se houver) será limitado a ser um tipo de Windows Runtime. Um valor booleano, por exemplo, é um tipo do Windows Runtime; mas um tipo projetado do C++/CX (por exemplo, Platform::Array<byte>^) não é.

Assincronia do C++/WinRT

O C++/WinRT integra as coroutinas C++ ao modelo de programação. As corrotinas e a instrução co_await fornecem uma forma natural de aguardar um resultado de modo cooperativo.

Cada um dos tipos IAsyncXxx é projetado em um tipo correspondente no namespace winrt::Windows::Foundation C++/WinRT. Vamos nos referir a eles como winrt::IAsyncXxx (em comparação com o IAsyncXxx^ de C++/CX).

O tipo de retorno de uma corrotina do C++/WinRT é um winrt::IAsyncXxx ou um winrt::fire_and_forget. E em vez de usar a return palavra-chave para retornar um objeto assíncrono, uma coroutina usa a co_return palavra-chave para retornar cooperativamente o valor que o chamador realmente deseja (talvez um arquivo, uma matriz de bytes ou um booliano).

Se um método contém pelo menos uma instrução co_await (ou pelo menos uma co_return ou co_yield), o método é uma corrotina por esse motivo.

Para obter mais informações e exemplos de código, consulte Simultaneidade e operações assíncronas com C++/WinRT.

O exemplo de jogo Direct3D (Simple3DGameDX)

Este tópico contém instruções passo a passo de várias técnicas de programação específicas que ilustram como portar gradualmente código assíncrono. Para servir como um estudo de caso, usaremos a versão C++/CX do exemplo de jogo Direct3D (que é chamado simple3DGameDX). Mostraremos alguns exemplos de como você pode usar o código-fonte C++/CX original nesse projeto e, gradualmente, portar seu código assíncrono para C++/WinRT.

  • Baixe o ZIP do link acima e descompacte-o.
  • Abra o projeto C++/CX (ele está na pasta denominada cpp) em Visual Studio.
  • Em seguida, você precisará adicionar suporte do C++/WinRT ao projeto. As etapas que você segue para fazer isso são descritas na tomada de um projeto C++/CX e na adição de suporte ao C++/WinRT. Nessa seção, a etapa sobre como adicionar o interop_helpers.h arquivo de cabeçalho ao seu projeto é particularmente importante porque dependeremos dessas funções auxiliares neste tópico.
  • Por fim, adicione #include <pplawait.h> a pch.h. Isso oferece suporte de corrotina para a PPL (há mais informações sobre esse suporte na seção a seguir).

Ainda não compile; caso contrário, você receberá erros sobre o byte ser ambíguo. Veja como resolver isso.

  • Abra BasicLoader.cpp e comente using namespace std;.
  • Nesse mesmo arquivo de código-fonte, você precisará qualificar shared_ptr como std::shared_ptr. Faça isso com Localizar e Substituir dentro desse arquivo.
  • Em seguida, qualifique o vetor como std::vector e a cadeia de caracteres como std::string.

O projeto agora é compilado novamente, tem suporte para C++/WinRT e contém as funções auxiliares de interoperabilidade from_cx e to_cx .

Agora você tem o projeto Simple3DGameDX pronto para acompanhar os passo a passo de código neste tópico.

Visão geral da migração de código assíncrono de C++/CX para C++/WinRT

Resumindo, conforme portamos, alteraremos as cadeias de tarefas PPL para chamadas a co_await. Vamos alterar o valor de retorno de um método de uma tarefa da PPL para um objeto winrt::IAsyncXxx do C++/WinRT. E também vamos alterar qualquer IAsyncXxx^ para um winrt::IAsyncXxx de C++/WinRT.

Você se lembrará de que uma corrotina é qualquer método que chama co_xxx. Uma coroutina C++/WinRT usa co_return para retornar seu valor de forma cooperativa. Graças ao suporte da corrotina para PPL(cortesia de pplawait.h), você também pode usar o co_return para retornar uma tarefa PPL de uma corrotina. E você também pode co_await as tarefas e IAsyncXxx. Mas você não pode usar co_return para retornar um IAsyncXxx^. A tabela a seguir descreve o suporte para interoperabilidade entre as várias técnicas assíncronas com pplawait.h na imagem.

Método Você pode fazer co_await dele? Você pode fazer co_return com base nele?
O método retorna Task<void> Yes Yes
Método retorna tarefa<T> No Yes
Método retorna IAsyncXxx^ Yes Não. Mas você envolve create_async em uma tarefa que usa co_return.
Método retorna winrt::IAsyncXxx Yes Yes

Use a tabela a seguir para ir diretamente para a seção deste tópico que descreve uma técnica de interoperabilidade de seu interesse ou simplesmente continuar a leitura a partir daqui.

Técnica de interoperabilidade assíncrona Seção deste tópico
Use co_await para aguardar um método tarefa<nulo> de dentro de um método fire-and-forget ou de um construtor. Aguardar task<void> dentro de um método fire-and-forget
Use co_await para aguardar um método task<void> de dentro de um método task<void>. Aguardar task<void> de um método task<void>
Use co_await para aguardar um método task<void> de dentro de um método task<T>. Aguardar task<void> de um método task<T>
Use co_await para aguardar um método do tipo IAsyncXxx^. Aguarde um IAsyncXxx^ em um método task, deixando o restante do projeto inalterado
Use co_return dentro de um método task<void>. Aguardar task<void> de um método task<void>
Use co_return em um método task<T>. Aguardar um IAsyncXxx^ em um método task, deixando o restante do projeto inalterado
Envolva create_async em uma tarefa que usa co_return. Envolva create_async em uma tarefa que usa co_return
Porte concurrency::wait. Migrar concurrency::wait para co_await winrt::resume_after
Retorne winrt::IAsyncXxx em vez de task<void>. Portar um tipo de retorno task<void> para winrt::IAsyncXxx
Converter um winrt::IAsyncXxx<T> (T é primitivo) em uma tarefa<T>. Converter um winrt::IAsyncXxx<T> (T é primitivo) em uma tarefa<T>
Converter um winrt::IAsyncXxx<T> (T é um tipo do Windows Runtime) em um task<T^>. Converter um winrt::IAsyncXxx<T> (T é um tipo do Windows Runtime) em um task<T^>

E aqui está um breve exemplo de código ilustrando parte do suporte.

#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

Mesmo com essas ótimas opções de interoperabilidade, a portabilidade gradualmente depende da escolha de alterações que podemos fazer cirurgicamente que não afetam o restante do projeto. Não queremos puxar uma ponta solta arbitrária que poderia, dessa forma, desfazer a estrutura de todo o projeto. Para isso, temos que fazer as coisas em uma ordem específica. Em seguida, examinaremos de perto alguns exemplos de alterações de portabilidade/interoperabilidade relacionadas à programação assíncrona.

Aguardar um método task<void>, deixando o restante do projeto inalterado

Um método que retorna task<void> executa trabalho de forma assíncrona e retorna um objeto de operação assíncrona, mas, em última análise, não produz um valor. Podemos co_await um método como esse.

Portanto, um bom lugar do qual começar a portabilidade do código assíncrono de maneira gradual é encontrar locais em que você pode chamar esses métodos. Esses lugares envolverão a criação e/ou o retorno de uma tarefa. Eles também podem envolver o tipo de cadeia de tarefas em que nenhum valor é passado de cada tarefa para sua continuação. Em situações como essa, você pode simplesmente substituir o código assíncrono por instruções co_await, como veremos.

Note

À medida que este tópico progride, você verá o benefício dessa estratégia. Quando um determinado método task<void> passar a ser chamado exclusivamente via co_await, você estará livre para portar esse método para C++/WinRT e fazê-lo retornar um winrt::IAsyncXxx.

Vamos encontrar alguns exemplos. Abra o projeto Simple3DGameDX (consulte o exemplo de jogo Direct3D).

Important

Nos exemplos a seguir, conforme você vê as implementações dos métodos que estão sendo alterados, tenha em mente que não precisamos alterar os chamadores dos métodos que estamos alterando. Essas alterações são locais e não se propagam pelo projeto.

Aguardar task<void> dentro de um método fire-and-forget

Vamos começar aguardando task<void> dentro de métodos fire-and-forget, pois este é o caso mais simples. Estes são métodos que funcionam de forma assíncrona, mas o chamador do método não espera que esse trabalho seja concluído. Basta chamar o método e esquecê-lo, embora ele seja concluído de forma assíncrona.

Observe a raiz do grafo de dependências do seu projeto em busca de métodos void que contenham create_task e/ou cadeias de tarefas nas quais apenas métodos task<void> são chamados.

No Simple3DGameDX, você encontrará um código como esse na implementação do método GameMain::Update. Ele está no arquivo GameMain.cppde código-fonte.

GameMain::Update

Aqui está um extrato da versão C++/CX do método, mostrando as duas partes do método que são concluídas de forma assíncrona.

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

Você pode ver uma chamada ao método Simple3DGame::LoadLevelAsync (que retorna uma task<void> PPL). Depois disso, há uma continuação que realiza um trabalho síncrono. LoadLevelAsync é assíncrono, mas não retorna um valor. Portanto, nenhum valor está sendo passado da tarefa para a continuação.

Podemos fazer o mesmo tipo de alteração no código nesses dois lugares. O código é explicado após a listagem abaixo. Poderíamos ter uma discussão sobre a maneira segura de acessar o ponteiro this em uma corrotina de membros de classe. Mas vamos adiar isso para uma seção mais adiante (a discussão que adiamos sobre co_await e o ponteiro this)—por enquanto, esse código funciona.

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

Como você pode ver, como LoadLevelAsync retorna uma tarefa, podemos co_await fazer isso. E não precisamos de uma continuação explícita — o código que vem após co_await só é executado quando LoadLevelAsync é concluído.

A introdução do co_await transforma o método em uma corrotina, portanto, não pudemos deixá-lo retornando void. É um método do tipo fire-and-forget, então o mudamos para retornar winrt::fire_and_forget.

Você também precisará editar GameMain.h. Altere o tipo de retorno de GameMain::Update de void para winrt::fire_and_forget na declaração lá também.

Você pode fazer essa alteração na sua cópia do projeto, e o jogo ainda compila e executa da mesma forma. O código-fonte ainda é fundamentalmente C++/CX, mas agora está usando os mesmos padrões que C++/WinRT, de modo que nos aproximou um pouco mais de poder portar o restante do código mecanicamente.

GameMain::ResetGame

GameMain::ResetGame é outro método de disparo e esquecimento; ele também chama LoadLevelAsync. Portanto, você poderá fazer a mesma alteração no código se quiser a prática.

GameMain::OnDeviceRestored

As coisas ficam um pouco mais interessantes em GameMain::OnDeviceRestored devido ao aninhamento mais profundo do código assíncrono, incluindo uma tarefa não operacional. Veja uma descrição das partes do método assíncronas (com o código síncrono menos interessante representado por reticências).

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

Primeiro, altere o tipo de retorno de GameMain::OnDeviceRestored de void para winrt::fire_and_forget em GameMain.h e .cpp. Você também precisará abrir DeviceResources.h e fazer a mesma alteração no tipo de retorno de IDeviceNotify::OnDeviceRestored.

Para portar o código assíncrono, remova todas as chamadas create_task e then e as chaves delas e simplifique o método para uma série simples de instruções.

Altere qualquer return que retorne uma tarefa para um(a) co_await. Você ficará com um return que não retorna nada, então exclua-o. Quando terminar, a tarefa não operacional terá desaparecido, e a descrição das partes assíncronas do método terá a seguinte aparência. Novamente, o código síncrono menos interessante é ignorado.

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

Como você pode ver, essa forma de estrutura assíncrona é significativamente mais simples e fácil de ler.

GameMain::GameMain

O construtor GameMain::GameMain executa o trabalho de forma assíncrona e nenhuma parte do projeto aguarda a conclusão desse trabalho. Novamente, essa listagem descreve as partes assíncronas.

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

Mas um construtor não pode retornar winrt::fire_and_forget, portanto vamos mover o código assíncrono para um novo método GameMain::ConstructInBackground do tipo fire-and-forget, reestruturar o código em instruções co_await e chamar o novo método a partir do construtor. Este é o resultado.

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

Agora, todos os métodos fire-and-forget, na verdade, todo o código assíncrono, em GameMain foi transformado em corrotinas. Se você tiver interesse, talvez possa procurar métodos fire-and-forget em outras classes e fazer alterações semelhantes.

A discussão adiada sobre o co_await e o ponteiro this

Quando estávamos fazendo alterações em GameMain::Update, adiei uma discussão sobre o ponteiro this. Vamos ter essa discussão aqui.

Isso se aplica a todos os métodos que alteramos até agora; e se aplica a todas as corrotinas, não apenas as do tipo fire-and-forget. Introduzir um co_await em um método introduz um ponto de suspensão. E por causa disso, temos que ter cuidado com o ponteiro this, do qual, é claro, fazemos uso depois do ponto de suspensão sempre que acessamos um membro da classe.

O resumo é que a solução é chamar implements::get_strong. Contudo, para uma discussão completa sobre o problema e a solução, confira Acessar com segurança o ponteiro this em uma corrotina de membro de classe.

Você pode chamar implements::get_strong somente em uma classe que deriva de winrt::implements.

Derivar GameMain de winrt::implements

A primeira alteração que precisamos fazer é em GameMain.h.

class GameMain :
    public DX::IDeviceNotify

GameMain continuará implementando DX::IDeviceNotify, mas vamos alterá-lo para derivar de winrt::implements.

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

Em seguida, em App.cpp, você encontrará este método.

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

Mas agora que o GameMain deriva de winrt::implements, precisamos construí-lo de uma maneira diferente. Nesse caso, usaremos o modelo de função winrt::make_self . Para obter mais informações, consulte instanciando e retornando interfaces e tipos de implementação.

Substitua essa linha de código por isso.

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

Para fechar o loop nessa alteração, também precisaremos alterar o tipo de m_main. Em App.h, você encontrará esse código.

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

Altere essa declaração de m_main para essa.

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

Agora podemos chamar implements::get_strong

Para GameMain::Update e para qualquer um dos outros métodos aos quais adicionamos um co_await, veja aqui como você pode chamar get_strong no início de uma corrotina para que uma referência forte sobreviva até a conclusão da corrotina.

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

Aguardar task<void> de um método task<void>

O próximo caso mais simples é aguardar task<void> dentro de um método que, por sua vez, retorna task<void>. Isso porque podemos fazer co_await de um task<void> e podemos fazer co_return dele.

Você encontrará um exemplo muito simples na implementação do método Simple3DGame::LoadLevelAsync. Ele está no arquivo Simple3DGame.cppde código-fonte.

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

Há apenas um código síncrono, seguido pelo retorno da tarefa criada por GameRenderer::LoadLevelResourcesAsync.

Em vez de retornar essa tarefa, nós fazemos co_await dela e, em seguida, co_return do resultante 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();
}

Isso não parece uma mudança profunda. Mas agora que estamos chamando GameRenderer::LoadLevelResourcesAsync via co_await, podemos adaptá-lo para passar a retornar um winrt::IAsyncXxx em vez de uma tarefa. Faremos isso mais tarde na seção Portar um tipo de retorno task<void> para winrt::IAsyncXxx.

Aguardar task<void> de um método task<T>

Embora não haja exemplos adequados a serem encontrados no Simple3DGameDX, podemos inventar um exemplo hipotético apenas para mostrar o padrão.

A primeira linha no exemplo de código abaixo demonstra o co_await simples de um task<void>. Em seguida, para satisfazer o tipo de retorno T< da tarefa>, precisamos retornar de forma assíncrona um StorageFile^. Para fazer isso, nós fazemos co_await de uma API do Windows Runtime e co_return do arquivo resultante.

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

Poderíamos até mesmo portar uma parte maior do método para C++/WinRT dessa forma.

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

O membro de dados m_renderer ainda é C++/CX naquele exemplo.

Aguardar um IAsyncXxx^ em um método task, deixando o restante do projeto inalterado

Vimos como você pode fazer co_await de task<void>. Você também pode fazer co_await de um método que retorna um IAsyncXxx, seja um método em seu projeto ou uma API assíncrona do Windows (por exemplo, StorageFolder.GetFileAsync, que aguardamos de modo cooperativo na seção anterior).

Para ver um exemplo de onde podemos fazer esse tipo de alteração no código, vamos analisar BasicReaderWriter::ReadDataAsync (você o encontrará implementado em BasicReaderWriter.cpp).

Aqui está a versão original do 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;
    });
}

A listagem de código abaixo mostra que é possível fazer co_await de APIs do Windows que retornam IAsyncXxx^. Não só isso, podemos também co_return o valor que BasicReaderWriter::ReadDataAsync retorna de forma assíncrona (nesse caso, uma matriz de bytes). Esta primeira etapa mostra como fazer apenas essas alterações; Na verdade, portaremos o código C++/CX para C++/WinRT na próxima seção.

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

Novamente, não precisamos alterar os chamadores dos métodos que estamos alterando, porque não alteramos o tipo de retorno.

Porta ReadDataAsync (principalmente) para C++/WinRT, deixando o restante do projeto inalterado

Podemos ir mais longe e portar o método quase inteiramente para C++/WinRT sem a necessidade de alterar qualquer outra parte do projeto.

A única dependência deste método em relação ao restante do projeto é o membro de dados BasicReaderWriter::m_location, que é um StorageFolder^ do C++/CX. Para deixar esse membro de dados inalterado e deixar o tipo de parâmetro e o tipo de retorno inalterados, precisamos executar apenas algumas conversões, uma no início do método e outra no final. Para isso, podemos usar as funções auxiliares de interoperabilidade from_cx e to_cx .

Veja como BasicReaderWriter::ReadDataAsync fica após portar sua implementação predominantemente para C++/WinRT. Este é um bom exemplo de migração gradual. E esse método está no estágio em que podemos deixar de pensar nele como um método C++/CX que usa algumas técnicas C++/WinRT e vê-lo como um método C++/WinRT que interopera com 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

Em ReadDataAsync acima, criamos e retornamos uma nova matriz C++/CX. E é claro que fazemos isso para satisfazer o tipo de retorno do método (para que não precisemos alterar o restante do projeto).

Você pode encontrar outros exemplos no seu próprio projeto em que, depois de portar, você chega ao final do método e tudo o que você tem é um objeto C++/WinRT. Para co_return isso, basta chamar to_cx para convertê-lo. Há mais informações sobre isso, e um exemplo, a próxima seção.

Converter um winrt::IAsyncXxx<T> em uma tarefa<T>

Esta seção lida com a situação em que você portou um método assíncrono para C++/WinRT (para que ele retorne um winrt::IAsyncXxx<T>), mas você ainda tem código C++/CX chamando esse método como se ele ainda estivesse retornando uma tarefa.

  • Um caso é onde T é primitivo, que não precisa de conversão.
  • O outro caso é em que T é um tipo de Windows Runtime, nesse caso, você precisará convertê-lo em um T^.

Converter um winrt::IAsyncXxx<T> (T é primitivo) em uma tarefa<T>

O padrão nesta seção se aplica quando você está retornando de forma assíncrona um valor primitivo (usaremos um valor booliano para ilustrar). Considere um exemplo em que um método que você já portau para C++/WinRT tem essa assinatura.

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

Você pode converter uma chamada para esse método em uma tarefa como esta.

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

Ou assim.

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

Observe que o tipo de retorno da tarefa da função lambda é explícito, pois o compilador não pode deduzê-la.

Também poderíamos chamar o método dentro de uma cadeia de tarefas arbitrária como esta. Novamente, com um tipo de retorno lambda explícito.

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

Converter um winrt::IAsyncXxx<T> (T é um tipo do Windows Runtime) em um task<T^>

O padrão nesta seção se aplica quando você está retornando de forma assíncrona um valor Windows Runtime (usaremos um valor StorageFile para ilustrar). Considere um exemplo em que um método que você já portau para C++/WinRT tem essa assinatura.

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

Esta próxima listagem mostra como converter uma chamada para esse método em uma tarefa. Observe que precisamos chamar a função auxiliar de interoperação to_cx para converter o objeto C++/WinRT retornado em um objeto handle de C++/CX (também conhecido como hat).

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

Aqui está uma versão mais sucinta disso.

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

E você pode até mesmo optar por encapsular esse padrão em um modelo de função reutilizável e fazer return dele da mesma forma que normalmente retornaria uma tarefa.

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

Se você gosta dessa ideia, talvez queira adicionar to_task a interop_helpers.h.

Envolva create_async em torno de uma tarefa que usa co_return

Não é possível fazer co_return de um IAsyncXxx^ diretamente, mas você pode chegar a algo semelhante. Se você tiver uma tarefa que retorne cooperativamente um valor, poderá encapsulá-la em uma chamada para concurrency::create_async.

Aqui está um exemplo hipotético, já que não há um exemplo que podemos tirar do Simple3DGameDX.

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

Como você pode ver, é possível obter o valor de retorno de qualquer método que você possa co_await.

Portar concurrency::wait para co_await winrt::resume_after

Há alguns pontos em que Simple3DGameDX usa concurrency::wait para pausar a thread por um breve período. Veja um exemplo.

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

A versão C++/WinRT de concurrency::wait é a estrutura winrt::resume_after. Podemos fazer co_await desse struct dentro de uma tarefa PPL. Aqui está um exemplo de código.

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

Observe as duas outras alterações que tivemos que fazer. Alteramos o tipo de GameConstants::InitialLoadingDelay para std::chrono::d uration e tornamos o tipo de retorno da função lambda explícito, pois o compilador não é mais capaz de deduzê-la.

Portar um tipo de retorno task<void> para winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

Nesta fase do nosso trabalho com Simple3DGameDX, todos os locais do projeto que chamam Simple3DGame::LoadLevelAsync usam co_await para chamá-lo.

Isso significa que podemos simplesmente alterar o tipo de retorno desse método de task<void> para winrt::Windows::Foundation::IAsyncAction (deixando o resto inalterado).

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

Agora deve ser relativamente simples migrar o restante desse método e suas dependências (como m_level e assim por diante) para C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Aqui está a versão original do C++/CX de 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 é o único lugar no projeto que chama GameRenderer::LoadLevelResourcesAsync e já usa co_await para chamá-lo.

Portanto, não é mais necessário que GameRenderer::LoadLevelResourcesAsync retorne uma tarefa — em vez disso, ela pode retornar uma winrt::Windows::Foundation::IAsyncAction. E a implementação em si é simples o suficiente para portar completamente para C++/WinRT. Isso envolve fazer a mesma alteração que fizemos em Portar concurrency::wait para co_await winrt::resume_after. E não há dependências significativas no restante do projeto para se preocupar.

Portanto, veja como o método fica depois de portá-lo completamente para 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);
}

A meta – portar totalmente um método para C++/WinRT

Vamos encapsular este passo a passo com um exemplo da meta final, portando totalmente o método BasicReaderWriter::ReadDataAsync para C++/WinRT.

Da última vez que examinamos esse método (na seção Porta ReadDataAsync (principalmente) para C++/WinRT, deixando o restante do projeto inalterado), ele foi portado principalmente para C++/WinRT. Mas ele ainda retornou uma tarefa de 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());
}

Em vez de retornar uma tarefa, vamos alterá-la para retornar uma IAsyncOperation. E em vez de retornar uma matriz de bytes por meio dessa IAsyncOperation, retornaremos um objeto C++/WinRT IBuffer . Isso também exigirá uma pequena alteração no código nos sites de chamada, como veremos.

Veja qual é a aparência do método após portar a implementação dele, o parâmetro dele e o membro de dados m_location para usar a sintaxe e os objetos do 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() };
}

Como você pode ver, BasicReaderWriter::ReadDataAsync em si é muito mais simples, pois consideramos em seu próprio método a lógica síncrona que recupera bytes do buffer.

Mas agora precisamos portar os sites de chamada desse tipo de estrutura no C++/CX.

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

Para esse padrão no C++/WinRT.

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

APIs importantes