Asynkronitet och samverkan mellan C++/WinRT och C++/CX

Tip

Även om vi rekommenderar att du läser det här avsnittet från början kan du gå direkt till en sammanfattning av interop-tekniker i avsnittet Översikt över portning av C++/CX-asynkronisering till C++/WinRT .

Det här är ett avancerat ämne som rör gradvis portning till C++/WinRT från C++/CX. Det här avsnittet tar upp var ämnet Interop mellan C++/WinRT och C++/CX slutar gälla.

Om storleken eller komplexiteten i din kodbas gör det nödvändigt att portera projektet gradvis behöver du en portningsprocess där C++/CX- och C++/WinRT-kod finns sida vid sida i samma projekt. Om du har asynkron kod kan du behöva ha aktivitetskedjor för Parallella mönsterbibliotek (PPL) och koroutiner finns sida vid sida i projektet när du gradvis porterar källkoden. Det här avsnittet fokuserar på tekniker för samverkan mellan asynkron C++/CX-kod och asynkron C++/WinRT-kod. Du kan använda dessa tekniker individuellt eller tillsammans. Med de här teknikerna kan du göra gradvisa, kontrollerade, lokala ändringar längs vägen mot att portera hela projektet, utan att varje ändring blir okontrollerad i hela projektet.

Innan du läser det här avsnittet är det en bra idé att läsa Interop mellan C++/WinRT och C++/CX. Det här avsnittet visar hur du förbereder projektet för gradvis portning. Dessutom introduceras två hjälpfunktioner som du kan använda för att konvertera ett C++/CX-objekt till ett C++/WinRT-objekt (och vice versa). Det här avsnittet om asynkron information bygger på den informationen och använder dessa hjälpfunktioner.

Note

Det finns vissa begränsningar för att gradvis portera från C++/CX till C++/WinRT. Om du har ett Windows Runtime komponentprojekt är det inte möjligt att portera gradvis, och du måste portera projektet i ett enda pass. Och för ett XAML-projekt måste dina XAML-sidtyper vara antingen alla C++/WinRT eller alla C++/CX. Mer information finns i avsnittet Flytta till C++/WinRT från C++/CX.

Anledningen till att ett helt avsnitt ägnas åt samverkan med asynkron kod

Det är i allmänhet enkelt att portera från C++/CX till C++/WinRT, med ett undantag för att flytta från PPL-uppgifter (Parallel Patterns Library) till coroutines. Modellerna är olika. Det finns ingen naturlig en-till-en-mappning från PPL-uppgifter till coroutines, och det finns inget enkelt sätt (som fungerar för alla fall) att mekaniskt portera koden.

Den goda nyheten är att konvertering från uppgifter till koroutiner leder till betydande förenklingar. Och utvecklingsteam brukar rapportera att när de väl har tagit sig över tröskeln med att portera sin asynkrona kod, är det återstående porteringsarbetet till stor del mekaniskt.

Ofta skrevs en algoritm ursprungligen för att passa synkrona API:er. Och sedan översattes det till uppgifter och explicita fortsättningar – resultatet blir ofta en oavsiktlig fördunkling av den underliggande logiken. Till exempel blir loopar rekursion; if-else grenar förvandlas till ett kapslat träd (en kedja) av uppgifter; delade variabler blir shared_ptr. För att dekonstruera den ofta onaturliga strukturen i PPL-källkoden rekommenderar vi att du först tar ett steg tillbaka och förstår avsikten med den ursprungliga koden (d.s. identifiera den ursprungliga synkrona versionen). Och sedan infoga co_await (kooperativt invänta) i lämpliga platser.

Om du därför har en C#-version (i stället för C++/CX) av den asynkrona kod som du vill starta porten från kan det ge dig en enklare tid och en renare port. C#-kod använder await. Så C#-kod följer redan i huvudsak en filosofi om att börja med en synkron version och sedan infoga await i lämpliga platser.

Om du inte har en C#-version av projektet kan du använda de tekniker som beskrivs i det här avsnittet. Och när du har portat till C++/WinRT blir strukturen för din asynkrona kod enklare att portera till C#, om du vill.

Viss bakgrund i asynkron programmering

För att ge oss en gemensam referensram för asynkrona programmeringsbegrepp och terminologi, låt oss först kort beskriva bakgrunden till asynkron programmering i Windows Runtime i allmänhet, och även hur de två C++-språkprojektionerna, på olika sätt, bygger ovanpå detta.

Projektet har metoder som fungerar asynkront och det finns två huvudsakliga typer.

  • Det är vanligt att du vill vänta tills asynkront arbete har slutförts innan du gör något annat. En metod som returnerar ett asynkront åtgärdsobjekt är en metod som du kan vänta på.
  • Men ibland vill eller behöver du inte vänta på att arbetet har slutförts asynkront. I så fall är det effektivare att den asynkrona metoden inte returnerar ett asynkront åtgärdsobjekt. En asynkron metod som den – en som du inte väntar på – kallas en fire-and-forget-metod.

Windows Runtime-asynkronobjekt (IAsyncXxx)

Namnområdet Windows::Foundation Windows Runtime innehåller fyra typer av asynkrona åtgärdsobjekt.

I det här avsnittet, när vi använder den praktiska förkortningen av IAsyncXxx, refererar vi antingen till dessa typer kollektivt; eller så pratar vi om en av de fyra typerna utan att behöva ange vilken.

C++/CX-asynkronisering

Asynkron C++/CX-kod använder PPL-uppgifter (Parallel Patterns Library). En PPL-aktivitet representeras av klassen concurrency::task.

Vanligtvis länkar en asynkron C++/CX-metod samman PPL-aktiviteter med hjälp av lambdafunktioner med concurrency::create_task och concurrency::task::then. Varje lambda-funktion returnerar en aktivitet som när den är klar genererar ett värde som sedan skickas till lambda för aktivitetens fortsättning.

I stället för att anropa create_task för att skapa en uppgift kan en asynkron C++/CX-metod anropa samtidighet::create_async för att skapa en IAsyncXxx^.

Därför kan returtypen för en asynkron C++/CX-metod vara en PPL-uppgift eller en IAsyncXxx^.

I båda fallen använder själva metoden nyckelordet return för att returnera ett asynkront objekt som när det är klart genererar det värde som anroparen faktiskt vill ha (kanske en fil, en matris med byte eller ett booleskt värde).

Note

Om en asynkron C++/CX-metod returnerar en IAsyncXxx^, begränsas TResult (om någon) till att vara en Windows Runtime typ. Ett booleskt värde är till exempel en Windows Runtime-typ, men en C++/CX-projicerad typ (till exempel Platform::Array<byte>^) är det inte.

C++/WinRT-asynkronisering

C++/WinRT integrerar C++-coroutines i programmeringsmodellen. Koroutiner och satsen co_await ger ett naturligt sätt att i samverkan vänta in ett resultat.

Var och en av IAsyncXxx-typerna projiceras till en motsvarande typ i namnområdet winrt::Windows::Foundation C++/WinRT. Låt oss referera till dem som winrt::IAsyncXxx (jämfört med IAsyncXxx^ av C++/CX).

Returtypen för en C++/WinRT-coroutine är antingen en winrt::IAsyncXxx eller winrt::fire_and_forget. I stället för att använda nyckelordet return för att returnera ett asynkront objekt använder en koroutin nyckelordet co_return för att tillsammans returnera det värde som anroparen faktiskt vill ha (kanske en fil, en matris med byte eller ett booleskt värde).

Om en metod innehåller minst en co_await -instruktion (eller minst en co_return eller co_yield) är metoden en coroutine av den anledningen.

Mer information och kodexempel finns i Samtidighet och asynkrona åtgärder med C++/WinRT.

Direct3D-spelexemplet (Simple3DGameDX)

Det här avsnittet innehåller genomgångar av flera specifika programmeringstekniker som visar hur du gradvis portar asynkron kod. För att fungera som en fallstudie använder vi C++/CX-versionen av Direct3D-spelexemplet (som kallas Simple3DGameDX). Vi visar några exempel på hur du kan ta den ursprungliga C++/CX-källkoden i projektet och gradvis portera dess asynkrona kod till C++/WinRT.

  • Ladda ned ZIP-filen från länken ovan och packa upp den.
  • Öppna C++/CX-projektet (det finns i mappen med namnet cpp) i Visual Studio.
  • Sedan måste du lägga till C++/WinRT-stöd i projektet. De steg som du följer för att göra som beskrivs i Ta ett C++/CX-projekt och lägga till C++/WinRT-stöd. I det avsnittet är steget om att lägga till interop_helpers.h huvudfilen i projektet särskilt viktigt eftersom vi kommer att vara beroende av dessa hjälpfunktioner i det här avsnittet.
  • Lägg slutligen till #include <pplawait.h> i pch.h. Det ger dig coroutine-stöd för PPL (det finns mer om det stödet i följande avsnitt).

Bygg inte än, annars får du felmeddelanden om att byte är tvetydigt. Så här löser du det.

  • Öppna BasicLoader.cpp och kommentera ut using namespace std;.
  • I samma källkodsfil måste du sedan kvalificera shared_ptr som std::shared_ptr. Du kan göra det med Sök och ersätt i den filen.
  • Kvalificera sedan vektor som std::vector och sträng som std::string.

Projektet byggs nu igen, har stöd för C++/WinRT och innehåller hjälpfunktionerna from_cx och to_cx interop.

Nu har du Simple3DGameDX-projektet redo att följa med i kodgenomgångarna i det här avsnittet.

Översikt över portning av C++/CX-asynkronisering till C++/WinRT

Kort sagt kommer vi i samband med porteringen att ändra PPL-uppgiftskedjor till anrop till co_await. Vi kommer att ändra returvärdet för en metod från en PPL-uppgift till ett C++/ WinRT-winrt::IAsyncXxx-objekt . Och vi kommer också att ändra alla IAsyncXxx^ till en C++/WinRT winrt::IAsyncXxx.

Du kommer ihåg att en coroutine är en metod som anropar co_xxx. En C++/WinRT-coroutine använder co_return för att kooperativt returnera sitt värde. Tack vare korutinstödet för PPL (tack vare pplawait.h) kan du även använda co_return för att returnera en PPL-aktivitet från en korutin. Och du kan även co_await både uppgifter och IAsyncXxx. Men du kan inte använda co_return för att returnera en IAsyncXxx^. Tabellen nedan beskriver stöd för interop mellan de olika asynkrona teknikerna med pplawait.h i bilden.

Metod Kan du co_await det? Kan du co_return göra det?
Metoden returnerar ogiltig aktivitet<> Yes Yes
Metod returnerar aktivitet<T> No Yes
Metoden returnerar IAsyncXxx^ Yes No. Men du omsluter create_async runt en uppgift som använder co_return.
Metoden returnerar winrt::IAsyncXxx Yes Yes

Använd nästa tabell för att gå direkt till avsnittet i det här avsnittet som beskriver en interop-teknik av intresse, eller bara fortsätta läsa härifrån.

Teknik för asynkron interoperabilitet Avsnitt i det här ämnet
Använd co_await för att avvakta en Task<void-metod> i en fire-and-forget-metod eller i en konstruktor. Vänta på aktivitets<tomrum> inom en metod för att utlösa och glömma
Använd co_await för att invänta en task<void>-metod från en task<void>-metod. Vänta på ogiltig aktivitet<> inom en aktivitets<void-metod>
Använd co_await för att invänta en Task<void>-metod från en Task<T>-metod. Vänta på ogiltig< aktivitet> i en aktivitets-T-metod<>
Använd co_await för att vänta på en IAsyncXxx^-metod. Vänta på en IAsyncXxx^ i en task-metod och lämna resten av projektet oförändrat
Använd co_return i en task<void>-metod. Använd await på task<void> i en task<void>-metod
Använd co_return i en task<T>-metod. Vänta på en IAsyncXxx^ i en aktivitetsmetod och lämna resten av projektet oförändrat
Omslut create_async runt en uppgift som använder co_return. Omslut create_async runt en uppgift som använderco_return
Portera concurrency::wait. Portera concurrency::wait till co_await winrt::resume_after
Returnera winrt::IAsyncXxx i stället för task<void>. Migrera en task<void>-returtyp till winrt::IAsyncXxx
Konvertera en winrt::IAsyncXxx<T> (T är primitiv) till en aktivitet<T>. Konvertera en winrt::IAsyncXxx<T> (T är primitiv) till en uppgift<T>
Omvandla en winrt::IAsyncXxx<T> (T är en Windows Runtime-typ) till en task<T^>. Konvertera en winrt::IAsyncXxx<T> (T är en Windows Runtime-typ) till en task<T^>

Och här är ett kort kodexempel som illustrerar en del av stödet.

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

Viktigt!

Även med dessa fantastiska alternativ för interoperabilitet beror en gradvis portering på att välja ändringar som vi kan genomföra selektivt utan att påverka resten av projektet. Vi vill undvika att dra i en godtycklig lös ände och därmed riva upp strukturen för hela projektet. För det måste vi göra saker i en viss ordning. Härnäst ska vi titta närmare på några exempel på hur du gör den här typen av asynkrona portnings-/interop-ändringar.

Invänta en task<void>-metod och lämna resten av projektet oförändrat

En metod som returnerar ogiltig aktivitet<> utför arbete asynkront och returnerar ett asynkront åtgärdsobjekt, men det genererar i slutändan inte något värde. Vi kan co_await en sådan metod.

Så ett bra ställe att börja portera asynkron kod gradvis är att hitta platser där du anropar sådana metoder. Dessa ställen kommer att innebära att man skapar och/eller återlämnar en uppgift. De kan också omfatta den typ av aktivitetskedja där inget värde skickas från varje aktivitet till dess fortsättning. På såna platser kan du bara ersätta asynkron kod med co_await -instruktioner, som vi ser.

Note

När det här avsnittet fortsätter ser du fördelarna med den här strategin. När en viss metod av typen task<void> anropas enbart via co_await, kan du sedan porta den metoden till C++/WinRT och låta den returnera en winrt::IAsyncXxx.

Nu ska vi hitta några exempel. Öppna Projektet Simple3DGameDX (se Direct3D-spelexemplet).

Viktigt!

I exemplen som följer, när du ser implementeringarna av metoder som ändras, bör du tänka på att vi inte behöver ändra anroparna för de metoder som vi ändrar. De här ändringarna lokaliseras och de överförs inte genom projektet.

Vänta på aktivitets<tomrum> inom en metod för att utlösa och glömma

Låt oss börja med att vänta på aktivitets<tomrum> inom fire-and-forget-metoder , eftersom det är det enklaste fallet. Det här är metoder som fungerar asynkront, men metodens anropare väntar inte på att arbetet ska slutföras. Du anropar bara metoden och glömmer den, trots att den slutförs asynkront.

Leta efter void metoder som innehåller create_task och/eller taskkedjor nära roten i projektets beroendegraf där endast metoder av typen task<void> anropas.

I Simple3DGameDX hittar du kod som den i implementeringen av metoden GameMain::Update. Den finns i källkodsfilen GameMain.cpp.

GameMain::Update

Här är ett utdrag från C++/CX-versionen av metoden som visar de två delarna av metoden som slutförs asynkront.

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

Du kan se ett anrop till metoden Simple3DGame::LoadLevelAsync (som returnerar en PPL-task<void>). Efter det kommer en fortsättning som utför visst synkront arbete. LoadLevelAsync är asynkront, men returnerar inte något värde. Därför skickas inget värde från aktiviteten till fortsättningen.

Vi kan göra samma typ av ändring av koden på dessa två platser. Koden förklaras efter listan nedan. Vi skulle kunna diskutera här hur man på ett säkert sätt kommer åt pekaren this i en korutin för en klassmedlem. Men låt oss skjuta upp det för ett senare avsnitt (Den uppskjutna diskussionen om co_await och den här pekaren)– för tillfället fungerar den här koden.

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

Som du ser kan vi det eftersom co_await returnerar en aktivitet. Och vi behöver ingen explicit fortsättning – koden som följer en co_await körs bara när LoadLevelAsync har slutförts.

Att införa co_await gör metoden till en coroutine, så kunde vi inte låta den returnera void. Det är en "fire-and-forget"-metod, så vi ändrade den så att den returnerar winrt::fire_and_forget.

Du måste också redigera GameMain.h. Ändra returtypen för GameMain::Update från void till winrt::fire_and_forget även i deklarationen.

Du kan göra den här ändringen i din kopia av projektet, och spelet kompileras och körs fortfarande på samma sätt. Källkoden är fortfarande i grunden C++/CX, men den använder nu samma mönster som C++/WinRT, så det har fört oss lite närmare att kunna porta resten av koden mekaniskt.

GameMain::ResetGame

GameMain::ResetGame är en annan fire-and-forget-metod; det anropar LoadLevelAsync också. Då kan du göra samma kodändring där också om du vill få lite övning.

GameMain::OnDeviceRestored

Det blir lite mer intressant i GameMain::OnDeviceRestored på grund av dess djupare kapsling av asynkron kod, inklusive en no-op uppgift. Här är en disposition av de asynkrona delarna av metoden (med den mindre intressanta synkrona koden som representeras av ellipser).

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

Ändra först returtypen för GameMain::OnDeviceRestored från void till winrt::fire_and_forget i GameMain.h och .cpp. Du måste också öppna DeviceResources.h och göra samma ändring i returtypen IDeviceNotify::OnDeviceRestored.

För att portera den asynkrona koden tar du bort alla anrop till create_task och then samt deras klammerparenteser och förenklar metoden till en rak serie satser.

Ändra alla return som returnerar en aktivitet till en co_await. Du får en return som inte returnerar någonting, så ta bara bort den. När du är klar kommer no-op-uppgiften att ha försvunnit, och dispositionen för de asynkrona delarna av metoden ser ut så här. Återigen utelämnas den mindre intressanta synkrona koden.

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

Som du ser är den här formen av asynkron struktur betydligt enklare och enklare att läsa.

GameMain::GameMain

Konstruktorn GameMain::GameMain utför arbete asynkront och ingen del av projektet väntar på att arbetet ska slutföras. Återigen beskriver den här listan de asynkrona delarna.

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

Men en konstruktor kan inte returnera winrt::fire_and_forget, så vi flyttar den asynkrona koden till en ny GameMain::ConstructInBackground fire-and-forget-metod, platta ut koden till co_await -instruktioner och anropa den nya metoden från konstruktorn. Här är resultatet.

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

Nu har alla ”fire-and-forget”-metoder – faktum är att all asynkron kod – i GameMain gjorts om till korutiner. Om du har lust, kanske du kan leta efter ”fire-and-forget”-metoder i andra klasser och göra motsvarande ändringar.

Den uppskjutna diskussionen om co_await och den här pekaren

När vi gjorde ändringar i GameMain::Update sköt jag upp en diskussion om den här pekaren. Låt oss ha den diskussionen här.

Detta gäller alla metoder som vi har ändrat hittills; och det gäller alla koroutiner, inte bara fire-and-forget-koroutiner. Att införa en co_await i en metod introducerar en suspensionspunkt. Och därför måste vi vara försiktiga med den här pekaren, som vi naturligtvis använder efter avstängningspunkten varje gång vi får åtkomst till en klassmedlem.

Kort sagt är lösningen att anropa implements::get_strong. Men för en fullständig diskussion om problemet och lösningen, se Säker åtkomst till this-pekaren i en klassmedlemskorutin.

Du kan anropa implementeringar::get_strong endast i en klass som härleds från winrt::implements.

Härled GameMain från winrt::implements

Den första ändringen vi behöver göra är i GameMain.h.

class GameMain :
    public DX::IDeviceNotify

GameMain fortsätter att implementera DX::IDeviceNotify, men vi ändrar det så att det härleds från winrt::implements.

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

Härnäst hittar du den här metoden i App.cpp.

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

Men nu när GameMain härleds från winrt::implements måste vi konstruera det på ett annat sätt. I det här fallet använder vi funktionsmallen winrt::make_self . Mer information finns i Instansiera och returnera implementeringstyper och gränssnitt.

Ersätt den kodraden med detta.

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

För att stänga loopen för ändringen måste vi också ändra typen av m_main. I App.hhittar du den här koden.

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

Ändra deklarationen för m_main till detta.

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

Nu kan vi anropa implements::get_strong

För GameMain::Update, och för någon av de andra metoderna som vi har lagt till en co_await i, så här anropar du get_strong i början av en korutin för att säkerställa att en stark referens förblir vid liv tills korutinen har slutförts.

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

Vänta på task<void> i en task<void>-metod

Nästa enklaste fall är att avvakta task<void> i en metod som själv returnerar task<void>. Det beror på att vi kan co_await ett Task<void>, och vi kan co_return från en.

Du hittar ett mycket enkelt exempel i implementeringen av metoden Simple3DGame::LoadLevelAsync. Den finns i källkodsfilen 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();
}

Det finns bara lite synkron kod, följt av att returnera uppgiften som skapas av GameRenderer::LoadLevelResourcesAsync.

I stället för att returnera den uppgiften co_await vi den och co_return sedan den resulterande 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();
}

Det ser inte ut som en djupgående förändring. Men nu när vi anropar GameRenderer::LoadLevelResourcesAsync via co_awaitkan vi porta den för att returnera en winrt::IAsyncXxx i stället för en uppgift. Det gör vi senare i avsnittet Portera typen task<void> som returtyp till winrt::IAsyncXxx.

Vänta på ogiltig< aktivitet> i en aktivitets-T-metod<>

Även om det inte finns några lämpliga exempel i Simple3DGameDX kan vi skapa ett hypotetiskt exempel bara för att visa mönstret.

Den första raden i kodexemplet nedan demonstrerar det enkla co_await i en task<void>. Sedan måste vi, för att uppfylla returtypen för task<T>, returnera en StorageFile^ asynkront. Det gör vi genom att co_await Windows Runtime-API:et och co_return den resulterande filen.

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

Vi kan till och med porta mer av metoden till C++/WinRT så här.

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

Den m_renderer datamedlemmen är fortfarande C++/CX i det exemplet.

Vänta på en IAsyncXxx^ i en aktivitetsmetod och lämna resten av projektet oförändrat

Vi har sett hur du kan co_awaitogiltigförklara< en uppgift>. Du kan också co_await en metod som returnerar en IAsyncXxx, oavsett om det är en metod i projektet eller ett asynkront API i Windows (till exempel StorageFolder.GetFileAsync, som vi inväntade tillsammans i föregående avsnitt).

För ett exempel på var vi kan göra den här typen av kodändring ska vi titta på BasicReaderWriter::ReadDataAsync (du hittar den implementerad i BasicReaderWriter.cpp).

Här är den ursprungliga C++/CX-versionen.

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

Kodlistan nedan visar att vi kan co_await Windows API:er som returnerar IAsyncXxx^. Inte bara det, vi kan också co_return det värde som BasicReaderWriter::ReadDataAsync returnerar asynkront (i det här fallet en matris med byte). Det här första steget visar hur du gör just dessa ändringar. Vi portar faktiskt C++/CX-koden till C++/WinRT i nästa avsnitt.

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

Återigen behöver vi inte ändra anroparna för de metoder som vi ändrar, eftersom vi inte ändrade returtypen.

Port ReadDataAsync (mestadels) till C++/WinRT, vilket lämnar resten av projektet oförändrat

Vi kan gå ett steg längre och portera metoden nästan helt till C++/WinRT utan att behöva ändra någon annan del av projektet.

Det enda beroende som den här metoden har för resten av projektet är BasicReaderWriter::m_location datamedlem, som är en C++/CX StorageFolder^. För att lämna datamedlemmen oförändrad och lämna parametertypen och returtypen oförändrad behöver vi bara utföra ett par konverteringar – en i början av metoden och en i slutet. För det kan vi använda hjälpfunktionerna from_cx och to_cx interop.

Så här ser BasicReaderWriter::ReadDataAsync ut efter att ha portat implementeringen främst till C++/WinRT. Detta är ett bra exempel på att portera gradvis. Och den här metoden är i ett skede där vi kan gå ifrån att tänka på den som en C++/CX-metod som använder vissa C++/WinRT-tekniker och se den som en C++/WinRT-metod som interoperates med 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

I ReadDataAsync ovan skapar och returnerar vi en ny C++/CX-matris. Och naturligtvis gör vi det för att uppfylla metodens returtyp (så att vi inte behöver ändra resten av projektet).

Du kan stöta på andra exempel i ditt eget projekt där du efter porteringen når slutet av metoden och allt du har är ett C++/WinRT-objekt. Det gör co_return du genom att anropa to_cx för att konvertera den. Det finns mer information om det och ett exempel på nästa avsnitt.

Konvertera en winrt::IAsyncXxx<T> till en uppgift<T>

Det här avsnittet behandlar situationen där du har portat en asynkron metod till C++/WinRT (så att den returnerar en winrt::IAsyncXxx<T>), men du har fortfarande C++/CX-kod som anropar metoden som om den fortfarande returnerar en uppgift.

  • Ett fall är där T är primitivt, vilket inte behöver någon konvertering.
  • Det andra fallet är där T är en Windows Runtime typ, i vilket fall du måste konvertera det till en T^.

Konvertera en winrt::IAsyncXxx<T> (T är primitiv) till en uppgift<T>

Mönstret i det här avsnittet gäller när du asynkront returnerar ett primitivt värde (vi använder ett booleskt värde för att illustrera). Tänk dig ett exempel där en metod som du redan har portat till C++/WinRT har den här signaturen.

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

Du kan konvertera ett anrop till den metoden till en uppgift som den här.

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

Eller så här.

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

Observera att uppgiftsreturtypen för lambda-funktionen är explicit, eftersom kompilatorn inte kan härleda den.

Vi kan också anropa metoden inifrån en godtycklig aktivitetskedja som den här. Återigen, med en explicit lambda-returtyp.

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

Omvandla en winrt::IAsyncXxx<T> (T är en Windows Runtime-typ) till en task<T^>

Mönstret i det här avsnittet gäller när du asynkront returnerar ett Windows Runtime värde (vi använder ett StorageFile-värde för att illustrera). Tänk dig ett exempel där en metod som du redan har portat till C++/WinRT har den här signaturen.

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

I nästa lista visas hur du konverterar ett anrop till den metoden till en uppgift. Observera att vi måste anropa funktionen to_cx interop helper för att konvertera det returnerade C++/WinRT-objektet till ett C++/CX-handtag (kallas även ett hattobjekt).

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

Här är en mer kortfattad version av det.

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

Och du kan till och med välja att kapsla in det mönstret i en återanvändbar funktionsmall och return returnera den på samma sätt som du normalt returnerar en 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());
}

Om du gillar den idén kanske du vill lägga till to_task i interop_helpers.h.

Omslut create_async runt en uppgift som använderco_return

Du kan inte co_return en IAsyncXxx^ direkt, men du kan uppnå något liknande. Om du har en aktivitet som samverkande returnerar ett värde kan du kapsla in den i ett anrop till concurrency::create_async.

Här är ett hypotetiskt exempel, eftersom det inte finns något exempel som vi kan lyfta från Simple3DGameDX.

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

Som du ser kan du få returvärdet från vilken metod som helst som du kan co_await.

Portera concurrency::wait till co_await winrt::resume_after

Det finns ett par platser där Simple3DGameDX använder samtidighet::vänta för att pausa tråden under en kort tid. Här följer ett exempel.

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

Motsvarigheten till concurrency::wait i C++/WinRT är strukturen winrt::resume_after. Vi kan co_await den strukturen i en PPL-uppgift. Här är ett kodexempel.

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

Observera de två andra ändringar som vi var tvungna att göra. Vi ändrade typen av GameConstants::InitialLoadingDelay till std::chrono::d uration, och vi gjorde returtypen för lambda-funktionen explicit, eftersom kompilatorn inte längre kan härleda den.

Portera en task<void>-returtyp till winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

I det här skedet av vårt arbete med Simple3DGameDX använder alla platser i projektet som anropar Simple3DGame::LoadLevelAsyncco_await för att göra det.

Det innebär att vi helt enkelt kan ändra metodens returtyp från aktivitets<tomrum> till winrt::Windows::Foundation::IAsyncAction (lämnar resten av det oförändrat).

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

Det bör nu vara ganska mekaniskt att porta resten av den metoden och dess beroenden (till exempel m_level och så vidare) till C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Här är den ursprungliga C++/CX-versionen av 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 är den enda platsen i projektet som anropar GameRenderer::LoadLevelResourcesAsync, och det använder co_await redan för att anropa det.

Det finns därför inte längre något behov av att GameRenderer::LoadLevelResourcesAsync returnerar en uppgift – den kan i stället returnera en winrt::Windows::Foundation::IAsyncAction. Och själva implementeringen är tillräckligt enkel för att porta helt till C++/WinRT. Det innebär att vi gör samma ändring som vi gjorde i Port concurrency::wait to co_await winrt::resume_after. Och det finns inga betydande beroenden för resten av projektet att oroa sig för.

Så här ser metoden ut efter att ha portat den helt till 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);
}

Målet – porta en metod helt till C++/WinRT

Låt oss avsluta den här genomgången med ett exempel på slutmålet genom att helt portera metoden BasicReaderWriter::ReadDataAsync till C++/WinRT.

Förra gången vi tittade på den här metoden (i avsnittet Port ReadDataAsync (mestadels) till C++/WinRT, vilket lämnade resten av projektet oförändrat, portades det mestadels till C++/WinRT. Men den gav fortfarande tillbaka en aktivitet av typen 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());
}

I stället för att returnera en aktivitet ändrar vi den så att den returnerar en IAsyncOperation. I stället för att returnera en matris med byte via IAsyncOperation returnerar vi i stället ett C++/WinRT IBuffer-objekt . Det kräver också en mindre ändring av koden på anropsplatserna, som vi ser.

Så här ser metoden ut efter att ha portat implementeringen, dess parameter och den m_location datamedlemmen för att använda C++/WinRT-syntax och -objekt.

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

Som du ser är BasicReaderWriter::ReadDataAsync själv mycket enklare, eftersom vi har räknat in den synkrona logiken som hämtar byte från bufferten i sin egen metod.

Men nu måste vi flytta över anropsplatserna från en sådan här struktur i C++/CX.

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

Till det här mönstret i C++/WinRT.

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

Viktiga API:er