Assíncronia e interoperabilidade entre C++/WinRT e C++/CX

Tip

Embora recomendemos que leia este tópico desde o início, pode ir diretamente para um resumo das técnicas de interoperabilidade na secção Visão Geral sobre a portabilidade de C++/CX assíncrono para C++/WinRT .

Este é um tema avançado relacionado com a adaptação gradual para C++/WinRT a partir de C++/CX. Este tema dá continuidade ao tópico Interoperabilidade entre C++/WinRT e C++/CX.

Se o tamanho ou complexidade da tua base de código tornar necessário portar o teu projeto gradualmente, então vais precisar de um processo de portabilidade em que, durante algum tempo, código C++/CX e C++/WinRT existam lado a lado no mesmo projeto. Se tiveres código assíncrono, então podes precisar que cadeias de tarefas e corutinas da Parallel Patterns Library (PPL) existam lado a lado no teu projeto à medida que vais portando gradualmente o código-fonte. Este tópico foca-se em técnicas de interoperabilidade entre código assíncrono C++/CX e código assíncrono C++/WinRT. Pode usar estas técnicas individualmente ou em conjunto. As técnicas permitem-lhe fazer alterações graduais, controladas e locais no processo de migrar todo o seu projeto, sem que cada alteração tenha um efeito em cascata descontrolado em todo o projeto.

Antes de leres este tópico, é boa ideia ler Interop entre C++/WinRT e C++/CX. Esse tema mostra-te como preparar o teu projeto para uma portabilidade gradual. Também introduz duas funções auxiliares que podes usar para converter um objeto C++/CX num objeto C++/WinRT (e vice-versa). Este tópico sobre assíncronia baseia-se nessa informação e utiliza essas funções auxiliares.

Note

Existem algumas limitações para portar gradualmente de C++/CX para C++/WinRT. Se tiveres um projeto de componente do Windows Runtime, então a portabilidade gradual não é possível, e terás de portar o projeto de uma só vez. E para um projeto XAML, em qualquer momento os tipos de página XAML devem ser todos C++/WinRT ou todos C++/CX. Para mais informações, consulte o tópico Passar de C++/CX para C++/WinRT.

A razão pela qual um tema inteiro é dedicado à interoperabilidade de código assíncrona

A migração de C++/CX para C++/WinRT é geralmente simples, com a única exceção da passagem das tarefas da Parallel Patterns Library (PPL) para corrotinas. Os modelos são diferentes. Não existe um mapeamento natural direto entre tarefas PPL e corrotinas, e não há uma forma simples (que funcione em todos os casos) de migrar o código de forma mecânica.

A boa notícia é que a conversão de tarefas em corrotinas resulta em simplificações significativas. E as equipas de desenvolvimento relatam rotineiramente que, uma vez superado o obstáculo de migrar o seu código assíncrono, o restante trabalho de migração é, em grande medida, mecânico.

Muitas vezes, um algoritmo foi originalmente escrito para se adequar a APIs síncronas. E depois isso foi traduzido em tarefas e continuações explícitas — o resultado muitas vezes era uma obstrução involuntária da lógica subjacente. Por exemplo, os ciclos tornam-se recursão; os ramos if-else transformam-se numa árvore aninhada (uma cadeia) de tarefas; variáveis partilhadas tornam-se shared_ptr. Para desconstruir a estrutura frequentemente artificial do código-fonte PPL, recomendamos que primeiro recue e compreenda a intenção do código original (ou seja, descubra a versão síncrona original). Em seguida, insira co_await (aguardar cooperativamente) nos pontos adequados.

Por essa razão, se tiveres uma versão C# (em vez de C++/CX) do código assíncrono a partir da qual deves começar a tua porta, isso pode facilitar-te a vida e ter 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 depois inserir await nos locais apropriados.

Se não tiver uma versão em C# do seu projeto, pode usar as técnicas descritas neste tópico. E depois de portares para C++/WinRT, a estrutura do teu código assíncrono será então mais fácil de portar para C#, se assim o desejares.

Alguma experiência em programação assíncrona

Para termos um referencial comum para conceitos e terminologia de programação assíncrona, vamos apresentar brevemente o cenário relativamente à programação assíncrona em Windows Runtime em geral, e também como as duas projeções da linguagem C++ estão, de forma diferente, sobrepostas a isso.

O teu projeto tem métodos que funcionam de forma assíncrona, e existem dois tipos principais.

  • É comum querer esperar pela conclusão do trabalho assíncrono antes de fazer outra coisa. Um método que devolve um objeto de operação assíncrona é um método cujo resultado pode ser aguardado.
  • Mas por vezes não se quer ou não se precisa de esperar que o trabalho seja concluído de forma assíncrona. Nesse caso, é mais eficiente para o método assíncrono não devolver um objeto de operação assíncrono. Um método assíncrono como esse — que não se espera — é conhecido como método de disparar e esquecer .

Objetos assíncronos do Windows Runtime (IAsyncXxx)

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

Neste tópico, quando usamos a abreviatura conveniente de IAsyncXxx, referimo-nos a estes tipos coletivamente; Ou estamos a falar de um dos quatro tipos sem precisar de especificar qual.

C++/CX assíncrono

O código C++/CX assíncrono utiliza tarefas da Biblioteca de Padrões Paralelos (PPL ). Uma tarefa PPL é representada pela classe concorrência::tarefa .

Normalmente, um método C++/CX assíncrono encadeia tarefas PPL entre si através da utilização de funções lambda com concurrency::create_task e concurrency::task::then. Cada função lambda devolve uma tarefa que, quando concluída, produz um valor que é depois passado para a lambda da continuação da tarefa.

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

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

Em qualquer dos casos, o próprio método usa a return palavra-chave para devolver um objeto assíncrono que, quando concluído, produz o valor que o chamador realmente quer (talvez um ficheiro, um array de bytes ou um Booleano).

Note

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

C++/WinRT assíncrono

O C++/WinRT integra as corrotinas de C++ no modelo de programação. Corrotinas e a co_await declaração fornecem uma forma natural de esperar cooperativamente por um resultado.

Cada um dos tipos IAsyncXxx é mapeado para um tipo correspondente no espaço de nomes winrt::Windows::Foundation de C++/WinRT. Vamos referir-nos a esses como winrt::IAsyncXxx (em comparação com o IAsyncXxx^ do C++/CX).

O tipo de retorno de uma corrotina C++/WinRT é ou winrt::IAsyncXxx, ou winrt::fire_and_forget. E em vez de usar a return palavra-chave para devolver um objeto assíncrono, uma cortina usa a co_return palavra-chave para devolver cooperativamente o valor que o chamador realmente quer (talvez um ficheiro, um array de bytes ou um Booleano).

Se um método contiver pelo menos uma co_await afirmação (ou pelo menos uma co_return ou co_yield), então o método é uma corrotina por essa razão.

Para mais informações e exemplos de código, veja Concorrência e operações assíncronas com C++/WinRT.

O exemplo do jogo Direct3D (Simple3DGameDX)

Este tópico contém guias de várias técnicas específicas de programação que ilustram como portar gradualmente código assíncrono. Para servir de estudo de caso, vamos usar a versão C++/CX do exemplo de jogo Direct3D (que se chama Simple3DGameDX). Vamos mostrar alguns exemplos de como podes pegar no código-fonte original C++/CX desse projeto e portar gradualmente o seu código assíncrono para C++/WinRT.

  • Descarregue o ficheiro ZIP da ligação acima e descomprima-o.
  • Abre o projeto C++/CX (está na pasta chamada cpp) no Visual Studio.
  • Depois terá de adicionar suporte a C++/WinRT ao projeto. Os passos que segue para o fazer estão descritos em Pegar num projeto C++/CX e adicionar suporte a C++/WinRT. Nessa secção, o passo sobre adicionar o interop_helpers.h ficheiro cabeçalho ao seu projeto é particularmente importante porque vamos depender dessas funções auxiliares neste tópico.
  • Finalmente, adicione #include <pplawait.h> a pch.h. Isso dá-te suporte de corrotina para PPL (há mais informações sobre esse apoio na secção seguinte).

Não compiles ainda, caso contrário vais obter erros a indicar que byte é ambíguo. Aqui está como resolver isso.

  • Abra BasicLoader.cpp e comente using namespace std;.
  • Nesse mesmo ficheiro de código-fonte, terá de qualificar shared_ptr como std::shared_ptr. Podes fazer isso com uma pesquisa e substituição dentro desse ficheiro.
  • Depois qualifica o vetor como std::vector, e a cadeia como std::string.

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

Agora já tem o projeto Simple3DGameDX pronto para acompanhar as explicações do código neste tópico.

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

Em suma, à medida que fizermos a migração, vamos converter as cadeias de tarefas PPL em chamadas a co_await. Vamos alterar o valor de retorno de um método de uma tarefa PPL para um objeto winrt::IAsyncXxx em C++/WinRT. E também vamos alterar qualquer IAsyncXxx^ para um winrt::IAsyncXxx em C++/WinRT.

Recordar-se-á de que uma corrotina é qualquer método que invoca co_xxx. Uma corrotina C++/WinRT usa co_return para devolver cooperativamente o seu valor. Graças ao suporte para corrotinas do PPL (graças a pplawait.h), também é possível usar co_return para devolver uma tarefa do PPL a partir de uma corrotina. E também é possível co_await usar tanto tarefas como IAsyncXxx. Mas não podes usar co_return para devolver um IAsyncXxx^. A tabela abaixo descreve o suporte para interoperabilidade entre as várias técnicas assíncronas com pplawait.h na imagem.

Método Consegues co_await ? Consegues co_return sair dela?
O método devolve task<void> Yes Yes
Método devolve task<T> No Yes
O método devolve IAsyncXxx^ Yes Não. Mas envolve create_async numa tarefa que usa co_return.
O método retorna winrt::IAsyncXxx Yes Yes

Use esta tabela seguinte para ir diretamente à secção deste tópico que descreve uma técnica de interoperabilidade de interesse, ou continue a ler a partir daqui.

Técnica de interoperabilidade assíncrona Secção deste tópico
Use co_await para aguardar um método de anulação< de tarefa> dentro de um método de disparar e esquecer, ou dentro de um construtor. Aguardar a tarefa<anulada> num método de disparar e esquecer
Use co_await para aguardar um método task<void> num método task<void>. Aguardar a tarefa<void> dentro de um método task<void>
Utilize co_await para aguardar um método task<void> dentro de um método task<T>. Aguardar a tarefa<void> dentro de um método da tarefa<T>
Use co_await para aguardar a conclusão de um método IAsyncXxx^. Aguardar um IAsyncXxx^ num método task, deixando o resto do projeto inalterado
Utilize co_return num método task<void>. Utilizar task<void> dentro de um método task<void>
Use co_return dentro de um método de tarefa<T> . Aguardar um IAsyncXxx^ num método de tarefa , deixando o resto do projeto inalterado
Envolva create_async em torno de uma tarefa que utiliza co_return. Encapsule create_async em torno de uma tarefa que usa co_return
Concorrência no porto::espera. Migrar concurrency::wait para co_await winrt::resume_after
Retorne winrt::IAsyncXxx em vez de task<void>. Converter um tipo de retorno task<void> em winrt::IAsyncXxx
Converter um winrt::IAsyncXxx<T> (T é primitivo) numa tarefa<T>. Converter um winrt::IAsyncXxx<T> (T é primitivo) numa tarefa<T>
Converter um winrt::IAsyncXxx<T> (T é um tipo de Windows Runtime) numa tarefa<T^>. Converter um winrt::IAsyncXxx<T> (T é um tipo de Windows Runtime) numa tarefa<T^>

Eis um pequeno exemplo de código que ilustra 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 estas ótimas opções de interoperabilidade, a portabilidade depende gradualmente da escolha de alterações que possamos fazer cirurgicamente e que não afetem o resto do projeto. Queremos evitar mexer numa ponta solta qualquer e, com isso, desmanchar toda a estrutura do projeto. Para isso, temos de fazer as coisas numa ordem específica. Em seguida, vamos analisar atentamente alguns exemplos de alterações deste tipo relacionadas com adaptação/interoperabilidade assíncrona.

Aguardar um método de anulação< de tarefa>, deixando o resto do projeto inalterado

Um método que devolve a tarefa<void> realiza o trabalho de forma assíncrona, e devolve um objeto de operação assíncrono, mas não produz um valor. Podemos co_await usar um método assim.

Assim, um bom ponto de partida para migrar gradualmente código assíncrono é encontrar pontos do código onde esses métodos são chamados. Esses locais implicarão criar e/ou devolver uma tarefa. Podem também envolver o tipo de cadeia de tarefas em que nenhum valor é passado de cada tarefa para a sua continuação. Em casos como esse, pode simplesmente substituir o código assíncrono por instruções co_await, como veremos.

Note

À medida que este tema avança, verá o benefício desta estratégia. Quando um determinado método task<void> passar a ser chamado exclusivamente através de co_await, pode então portar esse método para C++/WinRT e fazer com que devolva um winrt::IAsyncXxx.

Vamos encontrar alguns exemplos. Abra o projeto Simple3DGameDX (veja O exemplo do jogo Direct3D).

Important

Nos exemplos que se seguem, ao ver as implementações dos métodos a ser alteradas, tenha em mente que não precisamos de alterar o código que chama esses métodos. Estas mudanças são localizadas e não se espalham pelo projeto.

Aguardar a tarefa<anulada> num método de disparar e esquecer

Comecemos por aguardar task<void> em métodos fire-and-forget, já que 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. Limita-se a chamar o método e a esquecê-lo, embora seja concluído de forma assíncrona.

Procure na raiz do grafo de dependências do seu projeto métodos void que contenham create_task e/ou cadeias de tarefas em que apenas sejam chamados métodos task<void>.

No Simple3DGameDX, vais encontrar código assim na implementação do método GameMain::Update. Está no ficheiro GameMain.cppdo código-fonte.

GameMain::Atualização

Aqui está um excerto da versão C++/CX do método, mostrando as duas partes do método que se completam 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());
        ...
    ...
}

Pode ver-se uma chamada para o método Simple3DGame::LoadLevelAsync (que devolve uma PPL task<void>). Depois disso, há uma continuação que executa algum trabalho síncrono. O LoadLevelAsync é assíncrono, mas não devolve um valor. Portanto, nenhum valor é passado da tarefa para a continuação.

Podemos fazer o mesmo tipo de alteração ao código nestes dois locais. O código é explicado após a listagem abaixo. Poderíamos ter aqui uma discussão sobre a forma segura de aceder a este indicador numa corrotina de membro da turma. Mas vamos adiar isso para uma secção posterior (A discussão diferida sobre co_await e este ponteiro)—por agora, este 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 podem ver, porque o LoadLevelAsync devolve uma tarefa, podemos co_await fazê-la. E não precisamos de uma continuação explícita — o código que segue a co_await só se executa quando o LoadLevelAsync termina.

Introduzir o co_await transforma o método numa corrotina, por isso não podíamos deixá-lo voltar void. É um método fire-and-forget, por isso alterámo-lo para devolver winrt::fire_and_forget.

Também vais precisar de editar GameMain.h. Altera também o tipo de retorno de GameMain::Update de void para winrt::fire_and_forget na respetiva declaração.

Podes fazer esta alteração na tua cópia do projeto, e o jogo continua a construir e a correr da mesma forma. O código-fonte continua a ser fundamentalmente C++/CX, mas agora usa os mesmos padrões do C++/WinRT, o que nos aproximou um pouco de conseguir portar o resto do código mecanicamente.

GameMain::ResetGame

GameMain::ResetGame é outro método de disparar e esquecer; também chama LoadLevelAsync. Por isso, podes fazer a mesma alteração de código lá se quiseres praticar.

GameMain::OnDeviceRestored

As coisas ficam um pouco mais interessantes no GameMain::OnDeviceRestored devido ao seu aninhamento mais profundo de código assíncrono, incluindo uma tarefa no-op. Aqui está um esboço das partes assíncronas do método (com o código síncrono menos interessante representado por elipses).

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. Também terá de abrir DeviceResources.h e fazer a mesma alteração ao tipo de retorno de IDeviceNotify::OnDeviceRestored.

Para portar o código assíncrono, remova todas as create_task e depois chamadas e os seus parênteses curvados, e simplifique o método numa série plana de instruções.

Mude qualquer return que devolve uma tarefa para um co_await. Vais ficar com um return que não devolve nada, por isso apaga-o. Quando terminares, a tarefa no-op terá desaparecido e o contorno das partes assíncronas do método ficará assim. Mais uma vez, o código síncrono menos interessante é omitido.

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

Como podes ver, esta forma de estrutura assíncrona é significativamente mais simples e mais fácil de ler.

GameMain::GameMain

O construtor GameMain::GameMain executa o trabalho de forma assíncrona, e nenhuma parte do projeto espera que esse trabalho seja concluído. Mais uma vez, esta lista 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, por isso vamos mover o código assíncrono para um novo método fire-and-forget GameMain::ConstructInBackground, transformar o código em instruções co_await e chamar o novo método a partir do construtor. Aqui está 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 foram convertidos em corrotinas. Se assim te apetece, talvez possas procurar métodos de disparar e esquecer noutras disciplinas e fazer alterações semelhantes.

A discussão adiada sobre co_await e o ponteiro this

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

Isto aplica-se a todos os métodos que mudámos até agora; e aplica-se a todas as corrotinas, não apenas às “fire-and-forget”. Introduzir a co_await num método introduz um ponto de suspensão. E por isso, temos de ter cuidado com o ponteiro this, que naturalmente usamos após o ponto de suspensão sempre que acedemos a um membro da classe.

Resumindo, a solução é chamar implements::get_strong. Mas, para uma discussão completa da questão e da solução, veja Como aceder em segurança ao ponteiro this numa corrotina membro de uma classe.

Podes chamar implements::get_strong apenas numa classe que derive de winrt::implements.

Fazer GameMain derivar de winrt::implements

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

class GameMain :
    public DX::IDeviceNotify

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

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

A seguir, em App.cpp, 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 do winrt::implements, precisamos de o construir de uma forma diferente. Neste caso, vamos usar o modelo de função winrt::make_self . Para mais informações, consulte Instanciar e devolver tipos de implementação e interfaces.

Substitui essa linha de código por esta.

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

Para fechar o ciclo dessa mudança, também teremos de alterar o tipo de m_main. Em App.h, encontrará este código.

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

Muda essa declaração de m_main para esta.

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

Agora podemos invocar implements::get_strong

Para GameMain::Update, e para quaisquer outros métodos aos quais adicionámos co_await, eis como pode invocar get_strong no início de uma corrotina para garantir que uma referência forte se mantém até à conclusão da corrotina.

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

Aguardar a tarefa<void> dentro de um método task<void>

O próximo caso mais simples é aguardar task<void> dentro de um método que também devolve task<void>. Isto porque podemos co_await anular uma tarefa<>, e podemos co_return a partir de uma.

Encontrará um exemplo muito simples na implementação do método Simple3DGame::LoadLevelAsync. Está no ficheiro Simple3DGame.cppdo 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 algum código síncrono, seguido de devolução da tarefa criada pelo GameRenderer::LoadLevelResourcesAsync.

Em vez de devolver essa tarefa, co_await-la e, em seguida, co_return o void resultante.

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 a chamar GameRenderer::LoadLevelResourcesAsync através de co_await, podemos convertê-lo para devolver um winrt::IAsyncXxx em vez de uma tarefa. Faremos isso mais tarde na secção Migrar um tipo de retorno task<void> para winrt::IAsyncXxx.

Aguardar a tarefa<void> dentro de um método da tarefa<T>

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

A primeira linha no exemplo de código abaixo demonstra a simplicidade co_await de uma tarefa<void>. Depois, para satisfazer o tipo de retorno task<T>, precisamos de devolver de forma assíncrona um StorageFile^. Para isso, temos co_await uma API do Windows Runtime e co_return o ficheiro 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é portar mais do método para C++/WinRT assim.

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 m_renderer membro de dados continua a ser C++/CX nesse exemplo.

Aguardar um IAsyncXxx^ num método task, deixando o resto do projeto inalterado

Já vimos como se pode co_awaitanular a tarefa<>. Também pode co_await um método que devolve um IAsyncXxx, quer se trate de um método do seu projeto quer de uma API assíncrona do Windows (por exemplo, StorageFolder.GetFileAsync, que utilizámos com espera cooperativa na secção anterior).

Para um exemplo de onde podemos fazer este tipo de alteração de código, vejamos BasicReaderWriter::ReadDataAsync (vais encontrar implementado em BasicReaderWriter.cpp).

Aqui está a versão original em 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 podemos co_await APIs do Windows que devolvem IAsyncXxx^. Além disso, também podemos co_return obter o valor que o BasicReaderWriter::ReadDataAsync devolve de forma assíncrona (neste caso, um array de bytes). Este primeiro passo mostra como fazer exatamente essas alterações; na próxima secção, vamos portar o código C++/CX para 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;
}

Mais uma vez, não precisamos de mudar os chamadores dos métodos que estamos a mudar, porque não alterámos o tipo de retorno.

Portar o ReadDataAsync (maioritariamente) para C++/WinRT, deixando o resto do projeto inalterado

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

A única dependência deste método em relação ao resto do projeto é o membro de dados BasicReaderWriter::m_location , que é uma StorageFolder^ em C++/CX. Para manter esse elemento de dados inalterado, e para manter o tipo de parâmetro e o tipo de retorno inalterados, basta realizar 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 .

Eis como o BasicReaderWriter::ReadDataAsync se apresenta após portar a sua implementação predominantemente para C++/WinRT. Este é um bom exemplo de portabilidade gradual. E este método está numa fase em que podemos afastar-nos de o pensar 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

No ReadDataAsync acima, construímos e retornamos um novo array C++/CX. E claro que fazemos isso para satisfazer o tipo de retorno do método (para não termos de alterar o resto do projeto).

Podes encontrar outros exemplos no teu próprio projeto onde, depois de portar, chegas ao fim do método e tudo o que tens é um objeto C++/WinRT. Para co_return, basta chamar to_cx para o converter. Há mais informações sobre isso, e um exemplo, na próxima secção.

Converter um winrt::IAsyncXxx<T> numa tarefa<T>

Esta secção trata da situação em que portaste um método assíncrono para C++/WinRT (de modo a que devolve um winrt::IAsyncXxx<T>), mas ainda assim tens código C++/CX a chamar esse método como se ainda estivesse a devolver uma tarefa.

  • Um caso é quando T é primitivo, que não necessita de conversão.
  • O outro caso é quando T é um tipo do Windows Runtime, caso em que terá de convertê-lo em T^.

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

O padrão desta secção aplica-se quando está a devolver assíncronamente um valor primitivo (vamos usar um valor booleano para ilustrar). Considere um exemplo em que um método que já portou para C++/WinRT tem esta assinatura.

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

Podes converter uma chamada para esse método numa tarefa assim.

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

Note que o tipo de retorno de tarefa da função lambda é explícito, porque o compilador não o consegue deduzir.

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 de Windows Runtime) numa tarefa<T^>

O padrão desta secção aplica-se quando está a devolver assíncronamente um valor do Windows Runtime (vamos usar um valor StorageFile para ilustrar). Considere um exemplo em que um método que já portou para C++/WinRT tem esta 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 lista mostra como converter uma chamada para esse método numa tarefa. Tenha em atenção que precisamos de chamar a função auxiliar de interoperabilidade to_cx para converter o objeto C++/WinRT devolvido num objeto handle 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 podes até optar por encapsular esse padrão num template de função reutilizável e return retorná-lo tal como normalmente retornarias 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 gostas dessa ideia, talvez queiras adicionar to_task a interop_helpers.h.

Coloque create_async em torno de uma tarefa que use co_return

Não podes co_return usar um IAsyncXxx^ diretamente, mas podes obter algo semelhante. Se tiver uma tarefa que devolva cooperativamente um valor, pode encapsulá-la numa chamada a concurrency::create_async.

Aqui está um exemplo hipotético, já que não há nenhum exemplo que possamos retirar 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 pode ver, pode obter o valor de retorno de qualquer forma possível co_await.

Migrar concurrency::wait para co_await winrt::resume_after

Há alguns pontos em que Simple3DGameDX usa concurrency::wait para suspender a thread durante um curto período de tempo. Eis 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 corresponde à estrutura winrt::resume_after. Podemos co_await fazer essa estrutura 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);
    }));
    ...
}

Repara nas outras duas alterações que tivemos de fazer. Mudámos o tipo de GameConstants::InitialLoadingDelay para std::chrono::d uration, e tornámos explícito o tipo de retorno da função lambda, porque o compilador já não consegue deduzir.

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

Simple3DGame::LoadLevelAsync

Nesta fase do nosso trabalho com Simple3DGameDX, todos os pontos do projeto que chamam Simple3DGame::LoadLevelAsync usam co_await para o chamar.

Isto significa que podemos simplesmente mudar 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();
}

Deverá agora ser relativamente mecânico migrar o resto desse método e as suas dependências (como m_level, e assim por diante) para C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Aqui está a versão original em C++/CX do 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 local no projeto que chama GameRenderer::LoadLevelResourcesAsync e já usa co_await para o invocar.

Assim, já não há necessidade de o GameRenderer::LoadLevelResourcesAsync devolver uma tarefa — pode devolver um winrt::Windows::Foundation::IAsyncAction em vez disso. E a implementação em si é suficientemente simples para portar completamente para C++/WinRT. Isso envolve fazer a mesma alteração que fizemos em Port concurrency::wait para co_await winrt::resume_after. E não há dependências significativas do resto do projeto com que se possa preocupar.

Portanto, aqui está como o método funciona depois de o portar 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);
}

O objetivo — portar totalmente um método para C++/WinRT

Vamos terminar este guia com um exemplo do objetivo final, portando totalmente o método BasicReaderWriter::ReadDataAsync para C++/WinRT.

Da última vez que analisámos este método (na secção Port ReadDataAsync (maioritariamente) para C++/WinRT, deixando o resto do projeto inalterado), foi maioritariamente portado para C++/WinRT. Mas continuou a devolver 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 devolver uma Task, vamos alterá-la para que devolva uma IAsyncOperation. E, em vez de devolver um array de bytes por essa IAsyncOperation, passaremos a devolver um objeto C++/WinRT IBuffer. Isso também exigirá uma pequena alteração no código nos locais de chamada, como veremos.

Eis como fica o método depois de migrar a sua implementação, o seu parâmetro e o membro de dados m_location para utilizarem 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 pode ver, o próprio BasicReaderWriter::ReadDataAsync é muito mais simples, porque considerámos no seu próprio método a lógica síncrona que recupera bytes do buffer.

Mas agora precisamos de portar os sites de chamada a partir deste tipo de estrutura em C++/CX.

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

Para este 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