Asynchroniteit en interoperabiliteit tussen C++/WinRT en C++/CX

Hint

Hoewel we u aanraden dit onderwerp vanaf het begin te lezen, kunt u rechtstreeks naar een samenvatting van interoperabiliteitstechnieken gaan in het overzicht van het overzetten van C++/CX asynchroon naar C++/WinRT-sectie .

Dit is een geavanceerd onderwerp met betrekking tot het geleidelijk overzetten naar C++/WinRT van C++/CX. In dit onderwerp wordt uitgelegd waar het onderwerp Interop tussen C++/WinRT en C++/CX weggaat.

Als de grootte of complexiteit van uw codebase het nodig maakt om uw project geleidelijk te overzetten, hebt u een overdrachtsproces nodig waarin C++/CX- en C++/WinRT-code naast elkaar in hetzelfde project aanwezig zijn. Als u asynchrone code hebt, moet u mogelijk PPL-taakketens (Parallel Patterns Library) en coroutines naast elkaar in uw project hebben terwijl u uw broncode geleidelijk overzet. Dit onderwerp is gericht op technieken voor samenwerking tussen asynchrone C++/CX-code en asynchrone C++/WinRT-code. U kunt deze technieken afzonderlijk of samen gebruiken. Met de technieken kunt u geleidelijk, gecontroleerde, lokale wijzigingen aanbrengen op het pad naar het overzetten van uw hele project, zonder dat elke wijziging trapsgewijs oncontroleerbaar is gedurende het hele project.

Voordat u dit onderwerp leest, is het een goed idee om Interop te lezen tussen C++/WinRT en C++/CX. In dit onderwerp wordt beschreven hoe u uw project voorbereidt op geleidelijke overdracht. Er worden ook twee helperfuncties geïntroduceerd die u kunt gebruiken om een C++/CX-object te converteren naar een C++/WinRT-object (en omgekeerd). Dit onderwerp over asynchroniteit bouwt voort op die informatie en maakt gebruik van die helperfuncties.

Note

Er gelden enkele beperkingen voor het geleidelijk overzetten van C++/CX naar C++/WinRT. Als u een Windows Runtime onderdeelproject hebt, is het overzetten geleidelijk niet mogelijk en moet u het project in één keer overzetten. En voor een XAML-project moeten op elk gewenst moment uw XAML-paginatypen alle C ++/WinRT of alle C++/CX zijn. Zie het onderwerp Verplaatsen naar C++/WinRT van C++/CX voor meer informatie.

De reden waarom een volledig onderwerp gewijd is aan asynchrone code-interoperabiliteit

Het overzetten van C++/CX naar C++/WinRT is over het algemeen eenvoudig, met uitzondering van het verplaatsen van PPL-taken (Parallel Patterns Library) naar coroutines. De modellen zijn verschillend. Er is geen natuurlijke een-op-eenovereenkomst tussen PPL-taken en coroutines, en er is geen eenvoudige manier die in alle gevallen werkt om de code mechanisch over te zetten.

Het goede nieuws is dat conversie van taken naar coroutines leidt tot aanzienlijke vereenvoudigingen. En ontwikkelteams melden regelmatig dat de rest van het overdrachtswerk grotendeels mechanisch is zodra ze de horde van het overzetten van hun asynchrone code hebben overschreden.

Vaak is een algoritme oorspronkelijk geschreven voor synchrone API's. En dat werd omgezet in taken en expliciete vervolgen. Het resultaat is vaak een onbedoelde verdoezeling van de onderliggende logica. Zo worden lussen recursie; if-else-vertakkingen worden een geneste boomstructuur (een keten) van taken; gedeelde variabelen worden shared_ptr. Om de vaak onnatuurlijke structuur van PPL-broncode te deconstrueren, raden we u aan eerst terug te stappen en de intentie van de oorspronkelijke code te begrijpen (dat wil gezegd, de oorspronkelijke synchrone versie detecteren). Voeg vervolgens co_await (coöperatief wachten) in op de juiste plaatsen.

Als u daarom een C#-versie (in plaats van C++/CX) van de asynchrone code hebt waaruit u de poort wilt starten, kunt u hiermee een eenvoudigere tijd en een schonere poort maken. C#-code maakt gebruik van await. C#-code volgt dus al een filosofie om te beginnen met een synchrone versie en vervolgens op de juiste plaatsen in te await voegen.

Als u geen C#-versie van uw project hebt, kunt u de technieken gebruiken die in dit onderwerp worden beschreven. En zodra u bent overgezet naar C++/WinRT, is de structuur van uw asynchrone code dan gemakkelijker te overzetten naar C#, indien gewenst.

Enige achtergrond in asynchrone programmering

Om een gemeenschappelijk referentiekader te hebben voor concepten en terminologie rond asynchroon programmeren, schetsen we kort de context van asynchroon programmeren in Windows Runtime in het algemeen, en ook hoe de twee C++-taalprojecties daar elk op hun eigen manier op voortbouwen.

Uw project heeft methoden die asynchroon werken en er zijn twee hoofdtypen.

  • Het is gebruikelijk om te wachten op het voltooien van asynchroon werk voordat u iets anders doet. Een methode die een asynchroon bewerkingsobject retourneert, is een methode waarop u kunt wachten.
  • Maar soms wilt u niet of hoeft u niet te wachten tot asynchroon werk is voltooid. In dat geval is het efficiënter als de asynchrone methode niet een object voor een asynchrone bewerking retourneert. Een asynchrone methode zoals die—een methode waarop u niet wacht—staat bekend als een fire-and-forget-methode.

Windows Runtime async-objecten (IAsyncXxx)

De naamruimte Windows::Foundation Windows Runtime bevat vier typen asynchrone bewerkingsobjecten.

In dit onderwerp, wanneer we de handige afkorting van IAsyncXxx gebruiken, verwijzen we gezamenlijk naar deze typen; of we hebben het over een van de vier typen zonder op te geven welke.

C++/CX async

Asynchrone C++/CX-code maakt gebruik van PPL-taken (Parallel Patterns Library ). Een PPL-taak wordt weergegeven door de klasse concurrency::task.

Doorgaans koppelt een asynchrone C++/CX-methode PPL-taken aan elkaar met behulp van lambdafuncties met concurrency::create_task en concurrency::task::then. Elke lambda-functie retourneert een taak die, wanneer deze is voltooid, een waarde produceert die vervolgens wordt doorgegeven aan de lambda van de voortzetting van de taak.

In plaats van create_task aan te roepen om een taak te maken, kan een asynchrone C++/CX-methode gelijktijdigheid aanroepen::create_async om een IAsyncXxx^te maken.

Het retourtype van een asynchrone C++/CX-methode kan dus een PPL-taak of een IAsyncXxx^zijn.

In beide gevallen gebruikt de methode zelf het return trefwoord om een asynchroon object te retourneren dat, wanneer deze is voltooid, de waarde produceert die de aanroeper daadwerkelijk wil (misschien een bestand, een matrix van bytes of een Booleaanse waarde).

Note

Als een asynchrone C++/CX-methode een IAsyncXxx^retourneert, is de TResult (indien aanwezig) beperkt tot een Windows Runtime type. Een Booleaanse waarde is bijvoorbeeld een Windows Runtime type, maar een C++/CX-projecttype (bijvoorbeeld Platform::Array<byte>^) is dat niet.

C++/WinRT async

C++/WinRT integreert C++ coroutines in het programmeermodel. Coroutines en de instructie co_await bieden een natuurlijke manier om coöperatief op een resultaat te wachten.

Elk van de IAsyncXxx-typen wordt geprojecteerd in een bijbehorend type in de winrt::Windows::Foundation C++/WinRT-naamruimte. Laten we deze zien als winrt::IAsyncXxx (vergeleken met de IAsyncXxx^ van C++/CX).

Het retourtype van een C++/WinRT-coroutine is een winrt::IAsyncXxx of winrt::fire_and_forget. En in plaats van het return trefwoord te gebruiken om een asynchroon object te retourneren, gebruikt een coroutine het co_return trefwoord om samen de waarde te retourneren die de beller eigenlijk wil (misschien een bestand, een matrix van bytes of een Booleaanse waarde).

Als een methode ten minste één co_await-instructie bevat (of ten minste één co_return of co_yield), dan is de methode daarom een coroutine.

Zie Gelijktijdigheid en asynchrone bewerkingen met C++/WinRT voor meer informatie en codevoorbeelden.

Het Voorbeeld van het Direct3D-spel (Simple3DGameDX)

Dit onderwerp bevat overzichten van verschillende specifieke programmeertechnieken die laten zien hoe u geleidelijk asynchrone code kunt overzetten. Als casestudy gebruiken we de C++/CX-versie van het Direct3D-gamevoorbeeld (dat Simple3DGameDX wordt genoemd). We laten enkele voorbeelden zien van hoe u de oorspronkelijke C++/CX-broncode in dat project kunt gebruiken en de asynchrone code geleidelijk kunt overzetten naar C++/WinRT.

  • Download de ZIP via de bovenstaande koppeling en pak het uit.
  • Open het C++/CX-project (dit bevindt zich in de map met de naam cpp) in Visual Studio.
  • Vervolgens moet u C++/WinRT-ondersteuning toevoegen aan het project. De stappen die u volgt, worden beschreven in Het nemen van een C++/CX-project en het toevoegen van C++/WinRT-ondersteuning. In deze sectie is de stap voor het toevoegen van het interop_helpers.h headerbestand aan uw project bijzonder belangrijk, omdat we afhankelijk zijn van die helperfuncties in dit onderwerp.
  • Voeg tot slot toe #include <pplawait.h> aan pch.h. Dat geeft u coroutine-ondersteuning voor PPL (er is meer over die ondersteuning in de volgende sectie).

Voer nog geen build uit, anders krijgt u foutmeldingen dat byte dubbelzinnig is. U kunt dit als volgt oplossen.

  • Open BasicLoader.cppen maak commentaar using namespace std;.
  • In hetzelfde broncodebestand moet u vervolgens shared_ptr specificeren als std::shared_ptr. U kunt dit doen met zoeken en vervangen binnen dat bestand.
  • Vervolgens kwalificeren vector als std::vector en tekenreeks als std::string.

Het project wordt nu opnieuw gebouwd, heeft C++/WinRT-ondersteuning en bevat de from_cx - en to_cx helperfuncties voor interop.

U hebt nu het Simple3DGameDX-project gereed om de code-uitleg in dit onderwerp te volgen.

Overzicht van het overzetten van C++/CX asynchroon naar C++/WinRT

Kort gezegd zullen we tijdens het overzetten PPL-taakketens omzetten naar aanroepen van co_await. We wijzigen de retourwaarde van een methode van een PPL-taak in een C++/WinRT winrt::IAsyncXxx-object . En we veranderen ook IAsyncXxx^ in een C++/WinRT winrt::IAsyncXxx.

U zult zich herinneren dat een coroutine een methode is die co_xxx aanroept. Een C++/WinRT-coroutine gebruikt co_return om samen de waarde ervan te retourneren. Dankzij de coroutine-ondersteuning voor PPL (dankzij pplawait.h) kunt u met co_return ook vanuit een coroutine een PPL-taak retourneren. En u kunt ook co_await beide taken en IAsyncXxx. Maar u kunt co_return geen IAsyncXxx^ teruggeven. De onderstaande tabel beschrijft de ondersteuning voor interoperabiliteit tussen de verschillende asynchrone technieken die met pplawait.h op de afbeelding zijn aangegeven.

Methode Kun je co_await het? Kun je co_return ervan afhalen?
Methode geeft task<void> terug Yes Yes
Methode geeft task<T> terug No Yes
Methode retourneert IAsyncXxx^ Yes Nee. Maar u verpakt create_async om een taak heen die gebruikmaakt van co_return.
Methode geeft winrt::IAsyncXxx terug Yes Yes

Gebruik deze volgende tabel om rechtstreeks naar de sectie in dit onderwerp te gaan waarin een interessante interop-techniek wordt beschreven of lees verder.

Asynchrone interoptechniek Paragraaf in dit onderwerp
Gebruik co_await om een task<void>-methode af te wachten binnen een fire-and-forget-methode of binnen een constructor. Wacht op taak<ongeldigheid> binnen een methode voor fire-and-forget
Gebruik co_await om binnen een task<void>-methode te wachten op een task<void>-methode. Wacht op task<void> binnen een task<void>-methode
Gebruik co_await om te wachten op een task<void>-methode binnen een task<T>-methode. Wacht op task<void> binnen een task<T>-methode
Gebruik co_await dit om te wachten op een IAsyncXxx^-methode. Wacht op een IAsyncXxx^ in een taakmethode , waardoor de rest van het project ongewijzigd blijft
Gebruik co_return in een task<void>-methode. Gebruik await<Task> binnen een Task<void>-methode
Gebruik co_return binnen een task<T>-methode. Wacht op een IAsyncXxx^ in een taakmethode , waardoor de rest van het project ongewijzigd blijft
Plaats create_async om een taak die co_return gebruikt. Wikkel create_async om een taak die gebruikmaakt van co_return
Gelijktijdigheid van poort::wait. Zet concurrency::wait over naar co_await winrt::resume_after
Geef winrt::IAsyncXxx terug in plaats van task<void>. Converteer een task<void>-retourtype naar winrt::IAsyncXxx
Converteer een winrt::IAsyncXxx<T> (T is primitief) naar een taak<T>. Een winrt::IAsyncXxx<T> (T is primitief) converteren naar een taak<T>
Converteer een winrt::IAsyncXxx<T> (T is een Windows Runtime type) naar een taak<T^>. Een winrt::IAsyncXxx<T> (T is een Windows Runtime type) converteren naar een taak<T^>

Hier volgt een voorbeeld van een korte code waarin een deel van de ondersteuning wordt geïllustreerd.

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

Belangrijk

Zelfs met deze goede interoperabiliteitsopties hangt het geleidelijk overzetten af van het kiezen van wijzigingen die we zeer gericht kunnen aanbrengen, zonder dat dit gevolgen heeft voor de rest van het project. We willen voorkomen dat we aan een willekeurig los eindje trekken en daarmee de structuur van het hele project ontrafelen. Daarvoor moeten we dingen in een bepaalde volgorde doen. Hierna bekijken we enkele voorbeelden van het aanbrengen van dergelijke asynchrone overdrachts-/interopwijzigingen.

Wacht op een task<void>-methode, waarbij de rest van het project ongewijzigd blijft

Een methode die task<void> als retourwaarde heeft, voert werk asynchroon uit en retourneert een object voor een asynchrone bewerking, maar levert uiteindelijk geen waarde op. We kunnen co_await zo'n methode gebruiken.

Dus een goede plek om asynchrone code geleidelijk te overzetten, is door plaatsen te vinden waar u dergelijke methoden aanroept. Op die plaatsen gaat het om het maken en/of terugsturen van een taak. Ze kunnen ook betrekking hebben op het soort taakketen waarbij geen waarde van elke taak wordt doorgegeven aan de voortzetting ervan. In dergelijke plaatsen kunt u de asynchrone code vervangen door co_await instructies, zoals we zien.

Note

Naarmate dit onderwerp vordert, ziet u het voordeel van deze strategie. Zodra een bepaalde methode van het type task<void> uitsluitend via co_await wordt aangeroepen, kunt u die methode naar C++/WinRT porteren en deze een winrt::IAsyncXxx laten retourneren.

Laten we enkele voorbeelden vinden. Open het Simple3DGameDX-project (zie het Voorbeeld van het Direct3D-spel).

Belangrijk

In de volgende voorbeelden, zoals u ziet dat de implementaties van methoden worden gewijzigd, moet u er rekening mee houden dat we de aanroepers van de methoden die we wijzigen niet hoeven te wijzigen. Deze wijzigingen worden gelokaliseerd en ze lopen niet trapsgewijs door het project.

Gebruik await voor task<void> binnen een fire-and-forget-methode

Laten we beginnen met het gebruik van await bij task<void> in fire-and-forget-methoden, aangezien dat het eenvoudigste geval is. Dit zijn methoden die asynchroon werken, maar de aanroeper van de methode wacht niet totdat dat werk is voltooid. U roept de methode aan en vergeet deze, ondanks het feit dat deze asynchroon is voltooid.

Zoek in de basis van de afhankelijkheidsgrafiek van uw project naar voidmethoden die create_task bevatten en/of naar taakketens waarin alleen task<void>-methoden worden aangeroepen.

In Simple3DGameDX vindt u code zoals die in de implementatie van de methode GameMain::Update. Het bevindt zich in het broncodebestand GameMain.cpp.

GameMain::Update

Hier volgt een extract uit de C++/CX-versie van de methode, waarin de twee onderdelen van de methode worden weergegeven die asynchroon worden voltooid.

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

U ziet een aanroep naar de Simple3DGame::LoadLevelAsync-methode (die een PPL-taak<ongeldig retourneert>). Daarna komt er een voortzetting die synchroon werk verricht. LoadLevelAsync is asynchroon, maar retourneert geen waarde. Er wordt dus geen waarde doorgegeven van de taak aan de voortzetting.

We kunnen op deze twee plaatsen hetzelfde soort wijziging aanbrengen in de code. De code wordt uitgelegd na de onderstaande vermelding. We zouden het hier kunnen hebben over de veilige manier om de this-pointer te benaderen in een coroutine van een klasselid. Maar laten we dat uitstellen voor een latere sectie (de uitgestelde discussie over co_await en de aanwijzer)- voorlopig werkt deze code.

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

Zoals u kunt zien, omdat LoadLevelAsync een taak retourneert, kunnen co_await we dit doen. En we hebben geen expliciete voortzetting nodig: de code die volgt op een co_await uitvoering wordt alleen uitgevoerd wanneer LoadLevelAsync is voltooid.

Door de co_await te introduceren wordt de methode een coroutine, dus konden we deze niet void laten retourneren. Het is een fire-and-forget-methode, dus we hebben deze gewijzigd om winrt::fire_and_forget te retourneren.

U moet GameMain.h ook bewerken. Wijzig ook in de declaratie het retourtype van GameMain::Update van void naar winrt::fire_and_forget.

U kunt deze wijziging aanbrengen in uw kopie van het project en de game bouwt nog steeds en voert hetzelfde uit. De broncode is nog steeds fundamenteel C++/CX, maar gebruikt nu dezelfde patronen als C++/WinRT, waardoor we iets dichter zijn gekomen bij het mechanisch kunnen overzetten van de rest van de code.

GameMain::ResetGame

GameMain::ResetGame is nog een andere fire-and-forget-methode; het roept ook LoadLevelAsync aan. U kunt hier dus dezelfde codewijziging aanbrengen als u de oefening wilt uitvoeren.

GameMain::OnDeviceRestored

Dingen worden iets interessanter in GameMain::OnDeviceRestored vanwege het dieper nesten van asynchrone code, waaronder een no-op taak. Hier volgt een overzicht van de asynchrone onderdelen van de methode (met de minder interessante synchrone code die wordt vertegenwoordigd door weglatingstekens).

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

Wijzig eerst het retourtype van GameMain::OnDeviceRestored van void naar winrt::fire_and_forget in GameMain.h en .cpp. U moet ook DeviceResources.h openen en dezelfde wijziging aanbrengen aan het retourtype van IDeviceNotify::OnDeviceRestored.

Als u de asynchrone code wilt porteren, verwijdert u alle aanroepen van create_task en then en hun accolades, en vereenvoudigt u de methode tot een platte reeks instructies.

Wijzig elke return die een taak retourneert in een co_await. U blijft er een return achter die niets retourneert, dus verwijder dat gewoon. Wanneer u klaar bent, is de no-op taak verdwenen en ziet het overzicht van de asynchrone onderdelen van de methode er als volgt uit. Ook hier wordt de minder interessante synchrone code gewist.

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

Zoals u kunt zien, is deze vorm van asynchrone structuur aanzienlijk eenvoudiger en gemakkelijker te lezen.

GameMain::GameMain

De constructor GameMain::GameMain voert werkzaamheden asynchroon uit, en geen enkel onderdeel van het project wacht tot die werkzaamheden zijn voltooid. Nogmaals, in deze lijst worden de asynchrone onderdelen beschreven.

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

Maar een constructor kan geen winrt::fire_and_forget retourneren, dus verplaatsen we de asynchrone code naar een nieuwe Methode GameMain::ConstructInBackground fire-and-forget, plat de code in co_await instructies en roepen we de nieuwe methode aan vanuit de constructor. Dit is het resultaat.

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 zijn alle fire-and-forget-methoden, in feite alle asynchrone code, in GameMain omgezet in coroutines. Als u wilt, kunt u misschien op zoek gaan naar fire-and-forget-methoden in andere klassen en soortgelijke aanpassingen doorvoeren.

De uitgestelde discussie over co_await en de aanwijzer

Toen we wijzigingen aanbrachten in GameMain::Update, heb ik de discussie over de this-pointer uitgesteld. Laten we die discussie hier voeren.

Dit geldt voor alle methoden die we tot nu toe hebben gewijzigd; en het geldt voor alle koroutines, niet alleen voor fire-and-forget-coroutines. Door een co_await in een methode te introduceren, ontstaat een onderbrekingspunt. En daarom moeten we voorzichtig zijn met de aanwijzer, die natuurlijk na het schorsingspunt wordt gebruikt telkens wanneer we toegang hebben tot een klaslid.

Het korte verhaal is dat de oplossing implementaties aanroept::get_strong. Maar voor een volledige bespreking van het probleem en de oplossing, zie Veilig toegang krijgen tot de this-pointer in een coroutine van een klasselid.

U kunt implements::get_strong alleen aanroepen in een klasse die is afgeleid van winrt::implements.

GameMain afleiden van winrt::implements

De eerste wijziging die we moeten aanbrengen, bevindt zich in GameMain.h.

class GameMain :
    public DX::IDeviceNotify

GameMain blijft DX::IDeviceNotify implementeren, maar we wijzigen het om af te leiden van winrt::implements.

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

App.cppVervolgens vindt u deze methode.

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

Maar nu GameMain is afgeleid van winrt::implementeert, moeten we het op een andere manier bouwen. In dit geval zullen we de winrt::make_self-functiesjabloon gebruiken. Zie Instantiëren en retourneren van implementatietypen en interfaces voor meer informatie.

Vervang die coderegel door deze.

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

Als u de lus voor deze wijziging wilt sluiten, moet u ook het type m_main wijzigen. In App.h, vindt u deze code.

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

Wijzig deze verklaring van m_main in dit.

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

We kunnen nu implementaties aanroepen::get_strong

Voor GameMain::Update, en voor elk van de andere methoden waar we een co_await aan hebben toegevoegd, leest u hier hoe u get_strong aan het begin van een coroutine kunt aanroepen om ervoor te zorgen dat een sterke referentie behouden blijft totdat de coroutine is afgerond.

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

Wacht op task<void> binnen een task<void>-methode

Het volgende eenvoudigste geval is het awaiten van task<void> binnen een methode die zelf task<void> teruggeeft. Dat komt omdat we een > kunnen maken, en we kunnen co_return van een taak.

U vindt een zeer eenvoudig voorbeeld in de implementatie van de methode Simple3DGame::LoadLevelAsync. Het bevindt zich in het broncodebestand 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();
}

Er is alleen wat synchrone code, gevolgd door het teruggeven van de taak die wordt gemaakt door GameRenderer::LoadLevelResourcesAsync.

In plaats van de taak terug te geven, co_await we het, en vervolgens co_return we de resulterende 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();
}

Dat ziet er niet uit als een ingrijpende verandering. Maar nu we GameRenderer::LoadLevelResourcesAsync via co_await aanroepen, kunnen we deze aanpassen zodat deze een winrt::IAsyncXxx retourneert in plaats van een taak. Dat doen we later in de sectie Een task<void>-returntype omzetten naar winrt::IAsyncXxx.

Wacht op task<void> binnen een task<T>-methode

Hoewel er geen geschikte voorbeelden te vinden zijn in Simple3DGameDX, kunnen we een hypothetisch voorbeeld maken om het patroon weer te geven.

De eerste regel in het onderstaande codevoorbeeld toont de eenvoudige co_await van een task<void>. Als u vervolgens wilt voldoen aan het retourtype taak<T> , moeten we asynchroon een StorageFile^ retourneren. Om dat te doen, roepen we co_await een Windows Runtime-API aan en co_return het resulterende bestand.

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

We kunnen nog meer van de methode overzetten naar C++/WinRT zoals deze.

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

Het m_renderer gegevenslid is nog steeds C++/CX in dat voorbeeld.

Wacht op een IAsyncXxx^ in een taakmethode , waardoor de rest van het project ongewijzigd blijft

We hebben gezien hoe u de taak ongeldig kunt co_awaitmaken<>. U kunt ook co_await een methode die een IAsyncXxx retourneert, ongeacht of dat een methode in uw project is of een asynchrone Windows-API (bijvoorbeeld StorageFolder.GetFileAsync, die we in de vorige sectie gezamenlijk hebben afgewacht).

Als voorbeeld van waar we dit soort codewijziging kunnen aanbrengen, kijken we naar BasicReaderWriter::ReadDataAsync (je vindt die geïmplementeerd in BasicReaderWriter.cpp).

Dit is de oorspronkelijke C++/CX-versie.

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

In het onderstaande codevoorbeeld zien we dat we Windows-API's kunnen co_await die IAsyncXxx^ retourneren. Niet alleen dat, we kunnen ook co_return de waarde die BasicReaderWriter::ReadDataAsync asynchroon teruggeeft (in dit geval een byte-array). In deze eerste stap ziet u hoe u alleen die wijzigingen aanbrengt; In de volgende sectie gaan we de C++/CX-code daadwerkelijk overzetten naar 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;
}

Opnieuw hoeven we de aanroepers van de methoden die we wijzigen niet te wijzigen, omdat we het retourtype niet hebben gewijzigd.

Port ReadDataAsync (meestal) naar C++/WinRT, waardoor de rest van het project ongewijzigd blijft

We kunnen een stap verder gaan en de methode bijna volledig overzetten naar C++/WinRT zonder dat u een ander deel van het project hoeft te wijzigen.

De enige afhankelijkheid die deze methode heeft voor de rest van het project is de BasicReaderWriter::m_location gegevenslid, een C++/CX StorageFolder^. Als u dat gegevenslid ongewijzigd wilt laten en het parametertype en het retourtype ongewijzigd wilt laten, hoeven we slechts een aantal conversies uit te voeren, één aan het begin van de methode en één aan het einde. Hiervoor kunnen we de from_cx- en to_cx-interop-helperfuncties gebruiken.

Zo ziet BasicReaderWriter::ReadDataAsync eruit nadat de implementatie voornamelijk is overgezet naar C++/WinRT. Dit is een goed voorbeeld van het geleidelijk overzetten. En deze methode bevindt zich in het stadium waarin we er niet meer aan kunnen denken als een C++/CX-methode die gebruikmaakt van een aantal C++/WinRT-technieken en deze als een C++/WinRT-methode kunnen zien die interoperateert met 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

In ReadDataAsync hierboven maken en retourneren we een nieuwe C++/CX-matrix. En natuurlijk doen we dat om te voldoen aan het retourtype van de methode (zodat we de rest van het project niet hoeven te wijzigen).

U kunt andere voorbeelden tegenkomen in uw eigen project, waarbij u na het overzetten het einde van de methode bereikt en alles wat u hebt een C++/WinRT-object is. Om dat co_return te doen, roept u gewoon to_cx aan om dit te converteren. In de volgende sectie vindt u daar meer informatie over en een voorbeeld.

Een winrt::IAsyncXxx<T> converteren naar een taak<T>

Deze sectie behandelt de situatie waarin u een asynchrone methode hebt overgezet naar C++/WinRT (zodat deze een winrt::IAsyncXxx<T> retourneert), maar u nog steeds C++/CX-code hebt die deze methode aanroept alsof er nog steeds een taak wordt geretourneerd.

  • Een geval is waar T primitief is, die geen conversie nodig heeft.
  • In het andere geval is T een Windows Runtime type. In dat geval moet u dat converteren naar een T^.

Een winrt::IAsyncXxx<T> (T is primitief) converteren naar een taak<T>

Het patroon in deze sectie is van toepassing wanneer u asynchroon een primitieve waarde retourneert (we gebruiken een Booleaanse waarde om te illustreren). Bekijk een voorbeeld waarbij een methode die u al hebt overgezet naar C++/WinRT deze handtekening heeft.

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

U kunt een aanroep naar die methode converteren naar een taak zoals deze.

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

Of zoals dit.

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

U ziet dat het retourtype van de taak van de lambda-functie expliciet is, omdat de compiler deze niet kan afleiden.

We kunnen de methode ook aanroepen vanuit een willekeurige taakketen zoals deze. Nogmaals, met een expliciet lambda-retourtype.

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

Een winrt::IAsyncXxx<T> (T is een Windows Runtime type) converteren naar een taak<T^>

Het patroon in deze sectie is van toepassing wanneer u asynchroon een Windows Runtime waarde retourneert (we gebruiken een StorageFile-waarde om te illustreren). Bekijk een voorbeeld waarbij een methode die u al hebt overgezet naar C++/WinRT deze handtekening heeft.

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

In deze volgende vermelding ziet u hoe u een aanroep naar die methode converteert naar een taak. Merk op dat we de to_cx-interop-helperfunctie moeten aanroepen om het geretourneerde C++/WinRT-object te converteren naar een C++/CX-handleobject (ook wel een hoed genoemd).

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

Hier is een beknoptere versie hiervan.

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

En u kunt er zelfs voor kiezen om dat patroon in te pakken in een herbruikbare functiesjabloon, net return zoals u normaal gesproken een taak zou retourneren.

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

Als u dat idee leuk vindt, kunt u to_task toevoegen aan interop_helpers.h.

Plaats create_async rond een taak die co_return gebruikt

U kunt een co_return^ niet rechtstreeks gebruiken, maar u kunt iets dergelijks bereiken. Als u een taak hebt die gezamenlijk een waarde retourneert, kunt u deze inpakken binnen een aanroep naar gelijktijdigheid::create_async.

Hier volgt een hypothetisch voorbeeld, omdat er geen voorbeeld is dat we kunnen tillen van Simple3DGameDX.

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

Zoals u kunt zien, kunt u de retourwaarde verkrijgen van elke methode die u kunt co_awaitgebruiken.

Gelijktijdigheid van poort::wacht totco_await winrt::resume_after

Er zijn een paar plaatsen waar Simple3DGameDX gebruikmaakt van concurrency::wait om de thread korte tijd te onderbreken. Dit is een voorbeeld.

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

De C++/WinRT-versie van concurrency::wait is de struct winrt::resume_after. We kunnen co_await die struct in een PPL-task. Hier volgt een codevoorbeeld.

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

Let op de twee andere wijzigingen die we moesten aanbrengen. We hebben het type GameConstants::InitialLoadingDelay gewijzigd in std::chrono::d uration en we hebben het retourtype van de lambda-functie expliciet gemaakt, omdat de compiler deze niet meer kan afleiden.

Een task<void>-retourtype omzetten naar winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

In deze fase in ons werk met Simple3DGameDX gebruiken alle plaatsen in het project dat Simple3DGame::LoadLevelAsync aanroept co_await om het aan te roepen.

Dat betekent dat we het retourtype van die methode eenvoudig kunnen wijzigen van taak<void> naar winrt::Windows::Foundation::IAsyncAction (waardoor de rest ervan ongewijzigd blijft).

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

Het moet nu redelijk mechanisch zijn om de rest van die methode en de bijbehorende afhankelijkheden (zoals m_level, enzovoort) te overzetten naar C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Dit is de oorspronkelijke C++/CX-versie van 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 is de enige plek in het project waar GameRenderer::LoadLevelResourcesAsync wordt aangeroepen, en maakt al gebruik van co_await om dat te doen.

Dus hoeft GameRenderer::LoadLevelResourcesAsync niet langer een taak te retourneren; in plaats daarvan kan het een winrt::Windows::Foundation::IAsyncAction retourneren. En de implementatie zelf is eenvoudig genoeg om volledig naar C++/WinRT te worden overgezet. Dat houdt in dat we dezelfde wijziging aanbrengen als we hebben aangebracht in Port concurrency::wait naar co_await winrt::resume_after. En er zijn geen significante afhankelijkheden van de rest van het project waarover u zich zorgen hoeft te maken.

Zo ziet de methode eruit nadat deze volledig is overgezet naar 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);
}

Het doel: een methode volledig overzetten naar C++/WinRT

Laten we deze procedure afronden met een voorbeeld van het einddoel door de methode BasicReaderWriter::ReadDataAsync volledig over te zetten naar C++/WinRT.

De laatste keer dat we naar deze methode hebben gekeken (in de sectie Port ReadDataAsync (voornamelijk) naar C++/WinRT, waardoor de rest van het project ongewijzigd blijft), werd deze meestal overgezet naar C++/WinRT. Maar er werd nog steeds een taak van Platform::Array<byte>^ geretourneerd.

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

In plaats van een taak te retourneren, wordt deze gewijzigd om een IAsyncOperation te retourneren. En in plaats van een matrix van bytes te retourneren via die IAsyncOperation, retourneren we in plaats daarvan een C++/WinRT IBuffer-object . Hiervoor is ook een kleine wijziging in de code op de aanroepsites vereist, zoals we zullen zien.

Hier ziet u hoe de methode eruitziet na het overzetten van de implementatie, de parameter en het m_location gegevenslid om C++/WinRT-syntaxis en -objecten te gebruiken.

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

Zoals u kunt zien, is BasicReaderWriter::ReadDataAsync zelf veel eenvoudiger, omdat we hebben gekeken naar een eigen methode, de synchrone logica waarmee bytes uit de buffer worden opgehaald.

Maar nu moeten we de oproepsites van dit soort structuur in C++/CX overzetten.

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

Voor dit patroon in C++/WinRT.

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

Belangrijke API's