Aszinkronizálás és a C++/WinRT és a C++/CX közötti együttműködés

Tipp

Bár azt javasoljuk, hogy az elejétől olvassa el ezt a témakört, a C++/CX aszinkron és C++/WinRT közötti portolás áttekintésében közvetlenül az interoptikai technikák összefoglalására ugorhat.

Ez egy haladó téma, amely a C++/CX-ről a C++/WinRT-re való fokozatos áttéréssel kapcsolatos. Ez a témakör ott folytatódik, ahol a(z) A C++/WinRT és a C++/CX közötti együttműködés című témakör véget ér.

Ha a kódbázis mérete vagy összetettsége szükségessé teszi a projekt fokozatos portolását, akkor olyan portálási folyamatra lesz szüksége, amelyben egy ideig a C++/CX és a C+++/WinRT kód egymás mellett található ugyanabban a projektben. Ha aszinkron kódja van, akkor előfordulhat, hogy a forráskód fokozatos átültetése során a projektben egymás mellett kell jelen lenniük a Parallel Patterns Library (PPL) feladatláncainak és a korutinoknak. Ez a témakör az aszinkron C++/CX kód és az aszinkron C++/WinRT-kód közötti együttműködés technikáira összpontosít. Ezeket a technikákat egyenként vagy együtt is használhatja. A technikák lehetővé teszik, hogy fokozatos, ellenőrzött, helyi változtatásokat hajtson végre a teljes projekt portolása felé vezető úton anélkül, hogy minden módosítás ellenőrizetlenül kaszkádolt volna a projektben.

A témakör elolvasása előtt érdemes elolvasni a C++/WinRT és a C++/CX közötti Interop függvényt. Ez a témakör bemutatja, hogyan készítheti elő a projektet a fokozatos portolásra. Emellett két segédfüggvényt is bevezet, amelyekkel C++/CX objektumot konvertálhat C++/WinRT objektummá (és fordítva). Ez az aszinkron tartalomra vonatkozó témakör erre az információra épül, és ezeket a segédfüggvényeket használja.

Note

A C++/CX-ről a C+++/WinRT-be történő fokozatos portolásra bizonyos korlátozások vonatkoznak. Ha Windows-futtatókörnyezet összetevőprojekttel rendelkezik, akkor a portolás fokozatosan nem lehetséges, és a projektet egy lépésben kell portolnia. XAML-projekt esetén egy adott időpontban a XAML-oldaltípusoknak vagy mind C++/WinRT, vagy mind C++/CX típusúnak kell lenniük. További információ: Áthelyezés C++/WinRT-be C++/CX-ről.

Miért szentelünk egy teljes témát az aszinkron kód együttműködésének

A C++/CX-ről a C++/WinRT-be történő portolás általában egyszerű, azzal a kivétellel, hogy a párhuzamos minták kódtárának (PPL) feladatairól a koroutinokra kell áttérni. A modellek eltérőek. A PPL-feladatok és a korutinok között nincs természetes egy az egyben megfeleltetés, és nincs egyszerű mód arra, hogy a kódot mechanikusan átültessük úgy, hogy az minden esetben működjön.

A jó hír az, hogy a feladatok korutinokká alakítása jelentősen leegyszerűsíti a dolgokat. A fejlesztői csapatok pedig rutinszerűen jelentik, hogy ha már túl vannak az aszinkron kód portolásának akadályán, a portálási munka fennmaradó része nagyrészt mechanikus.

Gyakran előfordult, hogy egy algoritmus eredetileg a szinkron API-knak megfelelően lett megírva. Ezt aztán feladatokra és explicit folytatásokra fordították le – az eredmény gyakran a mögöttes logika véletlen elhomályosulása volt. Például a ciklusok rekurzióvá válnak; az if-else ágak a feladatok egymásba ágyazott fájává (láncává) alakulnak; a megosztott változók shared_ptr-vé válnak. A PPL-forráskód gyakran természetellenes szerkezetének dekonstruálásához javasoljuk, hogy először lépjen vissza, és ismerje meg az eredeti kód szándékát (azaz az eredeti szinkron verzió felderítését). Ezután illessze be a megfelelő helyekre a(z) co_await elemet (kooperatív várakozás).

Ezért ha az aszinkron kód C# (nem C++/CX) verziójával rendelkezik, amelyből megkezdheti a portot, az egyszerűbb időt és tisztább portot biztosít. A C#-kód a(z) await elemet használja. A C#-kód tehát már lényegében egy szinkron verzióval kezdődő, majd a megfelelő helyekre való beszúrás await filozófiáját követi.

Ha nem rendelkezik a projekt C#-verziójával, akkor a jelen témakörben ismertetett technikákat használhatja. És ha egyszer már átültette C++/WinRT-re, az aszinkron kód szerkezetét is könnyebb lesz C#-ra átültetni, ha úgy kívánja.

Némi háttér az aszinkron programozásban

Annak érdekében, hogy az aszinkron programozási fogalmak és terminológia közös referenciakerete legyen, röviden ismertessük a Windows-futtatókörnyezet aszinkron programozással kapcsolatos jelenetet általában, és azt is, hogy a két C++ nyelvi kivetítés hogyan van egymástól különböző módon rétegzve.

A projekt olyan metódusokkal rendelkezik, amelyek aszinkron módon működnek, és két fő típus létezik.

  • Gyakori, hogy várnia kell az aszinkron munka befejezésére, mielőtt valami mást tenne. Az aszinkron műveleti objektumot visszaadó metódus az, amellyel várni lehet.
  • Néha azonban nem szeretné vagy kell megvárni az aszinkron módon elvégzett munka befejezését. Ebben az esetben hatékonyabb, ha az aszinkron metódus nem aszinkron műveleti objektumot ad vissza. Az olyan aszinkron metódust, mint ez – amelyre nem vársz – fire-and-forget metódusnak nevezzük.

Windows-futtatókörnyezet aszinkron objektumok (IAsyncXxx)

Az Windows::Foundation Windows-futtatókörnyezet névtér négy aszinkron műveleti objektumtípust tartalmaz.

Ebben a témakörben, amikor az IAsyncXxx kényelmes rövidítését használjuk, vagy ezekre a típusokra hivatkozunk együttesen; vagy a négy típus egyikéről beszélünk anélkül, hogy meg kellene adnia, melyiket.

C++/CX aszinkron

Az aszinkron C++/CX-kód párhuzamos minták kódtári (PPL) feladatait használja. A PPL-feladatot a concurrency::task osztály reprezentálja.

A C++/CX aszinkron metódus tipikusan lambdafüggvényekkel, a concurrency::create_task és a concurrency::task::then használatával láncba fűzi a PPL-feladatokat. Minden lambda-függvény visszaad egy feladatot, amely a befejezéskor létrehoz egy értéket, amelyet aztán átad a tevékenység folytatásának lambdájába.

Másik lehetőségként a feladat létrehozásához a create_task meghívása helyett egy aszinkron C++/CX-metódus a concurrency::create_async függvényt hívhatja meg egy IAsyncXxx^ létrehozásához.

Az aszinkron C++/CX metódus visszatérési típusa lehet PPL-feladat vagy IAsyncXxx^.

Mindkét esetben maga a metódus a return kulcsszó használatával aszinkron objektumot ad vissza, amely a befejezett állapotban létrehozza a hívó által ténylegesen kívánt értéket (például egy fájlt, egy bájttömböt vagy egy logikai értéket).

Note

Ha egy aszinkron C++/CX metódus IAsyncXxx^-t ad vissza, akkor a TResult (ha van ilyen) Windows-futtatókörnyezet típusra korlátozódik. A logikai érték például Windows-futtatókörnyezet-típus, de a C++/CX leképezett típusa (például Platform::Array<byte>^) nem az.

C++/WinRT async

A C++/WinRT integrálja a C++ coroutinest a programozási modellbe. A coroutines és az co_await állítás természetes módot ad az eredmény kooperatív várakozására.

Az IAsyncXxx-típusok mindegyike a winrt::Windows::Foundation C++/WinRT névtér megfelelő típusába lesz kivetítve. Nevezzük ezeket winrt::IAsyncXxx-nek (szemben a C++/CX IAsyncXxx^-ével).

A C++/WinRT coroutine visszatérési típusa winrt::IAsyncXxx vagy winrt::fire_and_forget. És ahelyett, hogy a return kulcsszót használva aszinkron objektumot ad vissza, a koroutin a co_return kulcsszót használja a hívó által ténylegesen kívánt érték (például egy fájl, bájttömb vagy logikai) kooperatív visszaadására.

Ha egy metódus legalább egy co_await utasítást (vagy legalább egy co_return vagy co_yield elemet) tartalmaz, akkor a metódus emiatt korutin.

További információkért és kódpéldákért lásd a C++/WinRT egyidejűségi és aszinkron műveleteit.

A Direct3D játékminta (Simple3DGameDX)

Ez a témakör több olyan konkrét programozási technikát mutat be, amelyek bemutatják, hogyan lehet fokozatosan portolni az aszinkron kódot. Esettanulmányként a Direct3D játékminta C++/CX verzióját fogjuk használni (amelyet Simple3DGameDX-nek nevezünk). Bemutatunk néhány példát arra, hogyan használhatja az eredeti C++/CX forráskódot a projektben, és hogyan portozhatja fokozatosan az aszinkron kódját a C++/WinRT-be.

  • Töltse le a ZIP-t a fenti hivatkozásból, és bontsa ki.
  • Nyissa meg a C++/CX projektet (a nevesített cppmappában található) a Visual Studio.
  • Ezután C++/WinRT-támogatást kell hozzáadnia a projekthez. Az ehhez szükséges lépéseket a C++/CX-projekt használata és a C++/WinRT-támogatás hozzáadása című témakör ismerteti. Ebben a részben különösen fontos a interop_helpers.h fejlécfájl projekthez adásáról szóló lépés, mert ebben a témakörben ezekre a segédfüggvényekre támaszkodunk.
  • Végül adja hozzá a(z) #include <pplawait.h> elemet a(z) pch.h elemhez. Ez coroutine-támogatást nyújt a PPL-hez (erről a támogatásról a következő szakaszban olvashat bővebben).

Még ne indítsa el a buildet, különben olyan hibaüzenetet kap, hogy a byte nem egyértelmű. Ezt a következőképpen oldhatja meg.

  • Nyissa meg a BasicLoader.cpp elemet, és tegye megjegyzéssé a using namespace std; elemet.
  • Ugyanabban a forráskódfájlban ezután a shared_ptr elemet std::shared_ptr névvel kell minősítened. Ezt a fájlon belüli kereséssel és cserével teheti meg.
  • Ezután minősítse a vektortstd::vektornak, a sztring pedig std::karakterláncnak.

A projekt most újra létrejön, C++/WinRT-támogatással rendelkezik, és tartalmazza a from_cx és to_cx interop segítő függvényeket.

Most már készen áll a Simple3DGameDX projekt, így követni tudja az ebben a témakörben található kódbemutatókat.

A C++/CX aszinkron kód C++/WinRT-re történő átültetésének áttekintése

Dióhéjban: a portolás során a PPL-feladatláncokat co_await hívásaira fogjuk cserélni. Egy metódus visszatérési értékét PPL-tevékenységről C++/WinRT winrt::IAsyncXxx objektumra módosítjuk. Emellett az IAsyncXxx^ fájlokat is C++/WinRT winrt::IAsyncXxx típusúra módosítjuk.

Emlékezhet rá, hogy a coroutine minden olyan metódus, amely meghívja co_xxx. A C++/WinRT-korutin a co_return használatával kooperatív módon adja vissza az értékét. A PPL korutintámogatásának köszönhetően (pplawait.h jóvoltából) a co_return használatával egy korutinból PPL-feladatot is visszaadhat. És co_await mind a Task objektumokat, mind a IAsyncXxx használhatja. A co_return nem használható IAsyncXxx^ visszaadására. Az alábbi táblázat az ábrán szereplő pplawait.h és a különböző aszinkron technikák közötti interoperabilitás támogatását ismerteti.

Módszer Meg tudod co_await csinálni? co_return Tudsz belőle?
A metódus a tevékenység<érvénytelenségét> adja vissza Yes Yes
A metódus a T< feladatot> adja vissza No Yes
A metódus az IAsyncXxx értéket adja vissza^ Yes Nem. De a co_return elemet használó feladatot a create_async köré teszed.
A metódus winrt::IAsyncXxx értéket ad vissza Yes Yes

Ezt a következő táblázatot használva közvetlenül a témakör egy interop-technikát leíró szakaszára ugorhat, vagy csak folytathatja az olvasást innen.

Aszinkron interoperabilitási technika A témakör része
A co_await használatával megvárhat egy task<void> metódust egy fire-and-forget metóduson vagy egy konstruktoron belül. A feladat<üresen> hagyása egy tűz és felejtés metóduson belül
Használja a co_awaitawait kulcsszót egy task<void> metódus várakozásához egy task<void> metóduson belül. Tevékenység<érvénytelenítésének> várása egy tevékenység<érvénytelenítési> metódusán belül
A task<T> metóduson belüli task<void> metódusra való várakozáshoz használja a co_await elemet. task<void> várakoztatása egy task<T> metóduson belül
Az IAsyncXxx^ metódusra való várakozáshoz használja a(z) co_await elemet. Várjon egy IAsyncXxx^ elemre egy task metódusban, a projekt többi részének változatlanul hagyása mellett
Használja a co_return elemet egy task<void> metóduson belül. Tevékenység<érvénytelenítésének> várása egy tevékenység<érvénytelenítési> metódusán belül
Használja a co_return elemet egy task<T> metóduson belül. Várjon egy IAsyncXxx^ műveletre egy feladatmetódusban, a projekt többi részének változatlanul hagyásával
Burkold a(z) co_return elemet használó feladatot a create_async-be. A create_async köré csomagoljon egy co_return használó feladatot
concurrency::wait portolása. Portold át a concurrency::wait-et ide: co_await winrt::resume_after
A task<void> helyett winrt::IAsyncXxx értéket adjon vissza. A task<void> visszatérési típust winrt::IAsyncXxx-ra alakítása
A winrt::IAsyncXxx<T> (a T primitív típus) alakítsa át task<T> típussá. Konvertálja a(z) winrt::IAsyncXxx<T> típust (ahol a T primitív típus) task<T> típussá
Alakítsa át a winrt::IAsyncXxx<T> típust (ahol a T egy Windows-futtatókörnyezet-típus) task<T^> típussá. Alakítsa át a winrt::IAsyncXxx<T> típust (a T egy Windows-futtatókörnyezet-típus) task<T^> típussá

Íme egy rövid példa, amely a támogatás egy részét szemlélteti.

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

Fontos

Még ezekkel a nagyszerű interoperabilitási lehetőségekkel együtt is a fokozatos portolás attól függ, hogy ki tudunk-e választani olyan változtatásokat, amelyeket célzottan el lehet végezni anélkül, hogy a projekt többi részét érintenék. Szeretnénk elkerülni, hogy belekapaszkodjunk egy tetszőleges kilógó szálba, és ezzel megbontsuk az egész projekt szerkezetét. Ehhez bizonyos sorrendben kell tennünk a dolgokat. Következő lépésként áttekintünk néhány példát az aszinkron portolással/interop módosításokkal kapcsolatos ilyen típusú módosítások elvégzésére.

Várjon egy tevékenység<érvénytelenítési> metódusát, és hagyja változatlanul a projekt többi részét

A feladat-ürességet<> visszaadó metódus aszinkron módon végzi a munkát, és aszinkron műveleti objektumot ad vissza, de végül nem hoz létre értéket. Tudunk co_await egy ilyen módszert.

Így az aszinkron kód portolásának fokozatos elindításához érdemes olyan helyeket találni, ahol ilyen metódusokat hívunk. Ezek a helyek egy feladat létrehozását és/vagy visszaadását foglalják magukban. Emellett olyan tevékenységláncot is tartalmazhatnak, amelyben az egyes tevékenységek nem adnak át értéket a folytatásnak. Az ilyen helyeken egyszerűen lecserélheti az aszinkron kódot utasításokra co_await , ahogy látni fogjuk.

Note

A témakör előrehaladtával láthatja a stratégia előnyeit. Ha egy adott feladat<érvénytelenítési> metódusa kizárólag a C++/WinRT protokollon keresztül co_awaitlesz meghívva, akkor szabadon portozhatja a metódust, és egy winrt::IAsyncXxx értéket ad vissza.

Találjunk néhány példát. Nyissa meg a Simple3DGameDX projektet (lásd a Direct3D-játékmintát).

Fontos

Az alábbi példákban, amikor a metódusok implementációi módosulnak, ne feledje, hogy nem kell módosítani a módosított metódusok hívóit . Ezek a módosítások csak helyben érvényesek, és nem terjednek tovább a projektben.

A feladat<üresen> hagyása egy tűz és felejtés metóduson belül

Kezdjük a fire-and-forget metódusokon belüli task<void> awaitelésével, mivel ez a legegyszerűbb eset. Ezek olyan metódusok, amelyek aszinkron módon működnek, de a metódus hívója nem várja meg a munka befejezését. Csak hívja meg a metódust, és felejtse el, annak ellenére, hogy aszinkron módon fejeződik be.

Keressen a projekt függőségi gráfjának gyökerénél olyan voidmetódusokat, amelyek tartalmazzák a(z) create_task elemet, és/vagy olyan feladatláncokat, amelyekben csak task<void> metódusokat hívnak meg.

A Simple3DGameDX-ben ehhez hasonló kódot talál a GameMain::Update metódus implementációjában. A forráskódfájlban GameMain.cpptalálható.

GameMain::Update

Íme egy kivonat a metódus C++/CX verziójából, amely az aszinkron módon befejezett metódus két részét mutatja.

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

Megjelenik egy hívás a Simple3DGame::LoadLevelAsync metódushoz (amely a PPL-feladat<érvénytelenségét> adja vissza). Ez után egy folytatás , amely némi szinkron munkát végez. A LoadLevelAsync aszinkron, de nem ad vissza értéket. Így a tevékenység nem ad át értéket a folytatásnak.

Ezen a két helyen ugyanúgy módosíthatjuk a kódot. A kódot az alábbi felsorolás után ismertetjük. Beszélhetnénk itt a this mutató osztálytagi korutinban történő biztonságos eléréséről. De halasszuk ezt egy későbbi szakaszra (a co_await és a this mutató későbbre halasztott tárgyalása) – egyelőre ez a kód működik.

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

Mint látható, mivel a LoadLevelAsync egy feladatot ad vissza, meg tudjuk co_await csinálni. És nincs szükségünk explicit folytatásra – az azt co_await követő kód csak a LoadLevelAsync befejeződésekor lesz végrehajtva.

A co_await bevezetése korutinná alakítja a metódust, ezért nem hagyhattuk meg úgy, hogy void-t adjon vissza. Ez egy fire-and-forget metódus, ezért úgy módosítottuk, hogy winrt::fire_and_forget típust adjon vissza.

A(z) GameMain.h elemet is szerkesztenie kell majd. Változtassa meg a GameMain::Update visszatérési típusát void típusról winrt::fire_and_forget típusra ott is, a deklarációban.

Ezt a módosítást elvégezheti a projekt másolatán, és a játék továbbra is ugyanúgy épül és fut. A forráskód alapvetően még mindig C++/CX, de most ugyanazokat a mintákat használja, mint a C++/WinRT, így egy kicsit közelebb kerültünk ahhoz, hogy a kód többi részét mechanikusan is ki lehessen portosíteni.

GameMain::ResetGame

GameMain::ResetGame egy másik fire-and-forget metódus; a LoadLevelAsyncet is meghívja. Így ott is elvégezheted ugyanazt a kódmódosítást, ha szeretnél gyakorolni.

GameMain::OnDeviceRestored

A GameMain::OnDeviceRestored aszinkron kód mélyebb beágyazása miatt érdekesebbé válik, beleértve egy no-op feladatot is. Íme a módszer aszinkron részeinek vázlata (a kevésbé érdekes szinkron kód három ponttal jelölve).

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

Először módosítsa a GameMain::OnDeviceRestored visszatérési típusát void helyett winrt::fire_and_forget típusra a(z) GameMain.h és .cpp fájlban. Meg kell nyitnia a DeviceResources.h elemet is, és ugyanazt a módosítást kell elvégeznie az IDeviceNotify::OnDeviceRestored visszatérési típusán is.

Az aszinkron kód átültetéséhez távolítsa el az összes create_task és then hívást, valamint a hozzájuk tartozó kapcsos zárójeleket, és egyszerűsítse a metódust utasítások lineáris sorozatává.

Módosítson minden olyan return elemet, amely egy Taskot ad vissza, co_await elemmé. Marad egy return, amely nem ad vissza semmit, ezért egyszerűen töröld azt. Miután elkészült, a no-op feladat el fog tűnni, és a metódus aszinkron részeinek vázlata a következő lesz. Ismét kimarad a kevésbé érdekes szinkron kódrész.

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

Mint látható, az aszinkron struktúra ezen formája jelentősen egyszerűbb és könnyebben olvasható.

GameMain::GameMain

A GameMain::GameMain konstruktor aszinkron módon végzi a munkát, és a projekt egyetlen része sem várja meg a munka befejezését. Ez a lista ismét az aszinkron részeket ismerteti.

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

A konstruktorok azonban nem tudják visszaadni a winrt::fire_and_forget, ezért az aszinkron kódot egy új GameMain::ConstructInBackground fire-and-forget metódusba fogjuk áthelyezni, a kódot utasításokba co_await simítjuk, és meghívjuk az új metódust a konstruktortól. Itt az eredmény.

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

A GameMainben az összes tűz- és felejtési módszer – sőt, az összes aszinkron kód – koroutinokká lett alakítva. Ha úgy gondolja, esetleg megkereshetné a fire-and-forget metódusokat más osztályokban, és végezhetne hasonló módosításokat.

A co_await és a this mutatóról szóló elhalasztott vita

Amikor módosításokat végeztünk a GameMain::Update-ben, elhalasztottam egy beszélgetést erről a mutatóról. Beszéljük meg itt.

Ez az összes olyan metódusra vonatkozik, amelyet eddig módosítottunk; és minden korutinra is, nem csak az elindítás után magukra hagyottakra. A(z) co_await metódusba való bevezetése felfüggesztési pontot eredményez. Emiatt óvatosnak kell lennünk ezzel a mutatóval, amelyet természetesen a felfüggesztési pont után használunk minden alkalommal, amikor egy osztálytaghoz férünk hozzá.

Röviden, a megoldás az, hogy meg kell hívni a implements::get_strong függvényt. A probléma és a megoldás teljes körű ismertetését lásd itt: A this mutató biztonságos elérése egy osztálytag korutinban.

A implements::get_strong csak olyan osztályban hívható meg, amely a winrt::implements osztályból származik.

GameMain származtatása winrt::implements-ből

Az első módosítást a következőben GameMain.hkell elvégezni: .

class GameMain :
    public DX::IDeviceNotify

A GameMain továbbra is implementálja a DX::IDeviceNotify elemet, de a winrt::implements-ből származóra módosítjuk.

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

Ezután a(z) App.cpp elemben megtalálja ezt a metódust.

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

De most, hogy a GameMain a winrt::implements-ből származik, más módon kell felépítenünk. Ebben az esetben a winrt::make_self függvénysablont fogjuk használni. További információ: Implementációtípusok és felületek példányosítása és visszaadása.

Cserélje le azt a kódsort erre.

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

A változás ciklusának bezárásához meg kell változtatnunk a m_main típusát is. A(z) App.h részben találja ezt a kódot.

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

Módosítsa erre a m_main deklarációját.

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

Most már meghívhatjuk a implements::get_strong függvényt

A GameMain::Update metódus, valamint bármely más metódus esetében, amelyhez co_await-t adtunk, az alábbi módon hívhatja meg a get_strong függvényt a korutin elején annak biztosítására, hogy egy erős hivatkozás a korutin befejeződéséig fennmaradjon.

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

Tevékenység<érvénytelenítésének> várása egy tevékenység<érvénytelenítési> metódusán belül

A következő legegyszerűbb eset az, amikor egy olyan metóduson belül várunk a task<void>-ra, amely maga is task<void> típust ad vissza. Ez azért van, mert >, és az egyikből is.co_return

A Simple3DGame::LoadLevelAsync metódus implementációjában egy nagyon egyszerű példát talál. A forráskódfájlban Simple3DGame.cpptalálható.

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

Van néhány szinkron kód, amelyet a GameRenderer::LoadLevelResourcesAsync által létrehozott feladat visszaadása követ.

A feladat visszaadása helyett co_await, majd co_return az eredményül kapott 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();
}

Ez nem úgy néz ki, mint egy mély változás. De most, hogy a GameRenderer::LoadLevelResourcesAsync metódust a co_await használatával hívjuk meg, szabadon átalakíthatjuk úgy, hogy task helyett winrt::IAsyncXxx típust adjon vissza. Ezt később, a A task<void> visszatérési típus winrt::IAsyncXxx típusra való portolása szakaszban tesszük meg.

A tevékenység<érvénytelenné tétele> egy T< tevékenységmetódusban>

Bár a Simple3DGameDX-ben nincsenek megfelelő példák, a minta megjelenítéséhez létrehozhatunk egy hipotetikus példát.

Az alábbi kódpélda első sora a task<void> egyszerű co_await használatát mutatja be. Ezután a T< feladat> visszatérési típusának kielégítése érdekében aszinkron módon vissza kell adnunk egy StorageFile^-t. Ehhez egy Windows-futtatókörnyezet API-t és co_await az eredményként kapott fájlt.co_return

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

Akár a módszer még nagyobb részét is átültethetnénk C++/WinRT-re így.

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

A m_renderer adattag abban a példában még mindig C++/CX-típusú.

Várakoztasson egy IAsyncXxx^ elemet egy task metódusban, miközben a projekt többi része változatlan marad

Láttuk, hogyan >. Olyan metódust is co_await megadhat, amely egy IAsyncXxx-et ad vissza, legyen az egy metódus a projektben, vagy egy aszinkron Windows API (például StorageFolder.GetFileAsync, amelyet az előző szakaszban közösen vártunk).

Egy példa arra, hogy hol végezhetjük el az ilyen típusú kódmódosításokat, tekintsük meg a BasicReaderWriter::ReadDataAsync (a implementálást BasicReaderWriter.cppitt találja).

Itt található az eredeti C++/CX verzió.

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

Az alábbi kódlista azt mutatja, hogy co_await használhatunk IAsyncXxx^-t visszaadó Windows API-kat. Nemcsak erről van szó, hanem co_return aszinkron módon lekérhetjük a BasicReaderWriter::ReadDataAsync által visszaadott értéket is (ebben az esetben egy bájttömböt). Ez az első lépés megmutatja, hogyan végezze el csak ezeket a módosításokat; a C++/CX kódot ténylegesen a következő szakaszban ültetjük át C++/WinRT-re.

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

Ismét nem kell módosítani a módosított metódusok hívóit , mert nem módosítottuk a visszatérési típust.

ReadDataAsync (többnyire) C++/WinRT-re portolása, a projekt többi részének változatlanul hagyásával

Egy lépéssel továbbhaladhatunk, és szinte teljes egészében a C++/WinRT-hez portozhatjuk a metódust anélkül, hogy a projekt bármely más részét módosítani kellene.

A projekt többi részétől ez a metódus csak a BasicReaderWriter::m_location adattagtól függ, amely egy C++/CX StorageFolder^. Ha változatlanul szeretné hagyni az adattagot, és változatlanul szeretné hagyni a paramétertípust és a visszatérési típust, csak néhány átalakítást kell végrehajtanunk – egyet a metódus elején, egyet pedig a végén. Ehhez használhatjuk a from_cx és to_cx interop segítő függvényeket.

Így néz ki a BasicReaderWriter::ReadDataAsync, miután a megvalósítását nagyrészt C++/WinRT-re portolták. Ez jó példa a fokozatos portolásra. És ez a módszer abban a szakaszban van, ahol el tudunk mozdulni attól, hogy C++/CX metódusként tekintsünk rá, amely C+++/WinRT technikákat használ, és úgy tekintünk rá, mint egy C++/WinRT metódusra, amely együttműködik a C++/CX-vel.

#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

A fenti ReadDataAsyncben létrehozunk és visszaadunk egy új C++/CX tömböt. És természetesen ezt azért tesszük, hogy kielégítsük a metódus visszatérési típusát (hogy ne kelljen a projekt többi részét módosítani).

Más példákat is láthat a saját projektjében, ahol a portolás után eléri a metódus végét, és mindössze egy C++/WinRT-objektummal rendelkezik. Ehhez a co_return csak hívja meg a to_cx függvényt annak átalakításához. Erről további információ és egy példa a következő szakasz.

winrt::IAsyncXxx<T> átalakítása task<T> típussá

Ez a szakasz azokkal a helyzetekkel foglalkozik, amikor aszinkron metódust portolt a C++/WinRT-be (így egy winrt::IAsyncXxx<T> értéket ad vissza), de továbbra is C++/CX-kód hívja meg a metódust, mintha továbbra is egy feladatot adna vissza.

  • Az egyik eset az, amikor a T primitív, amely nem igényel konverziót.
  • A másik eset az, amikor a T egy Windows-futtatókörnyezet típus, ilyenkor azt T^-vé kell alakítania.

winrt::IAsyncXxx<T> (ahol a T primitív típus) konvertálása task<T> típusra

Az ebben a szakaszban szereplő minta akkor érvényes, ha aszinkron módon ad vissza egy primitív értéket (a szemléltetéshez logikai értéket használunk). Vegyünk egy példát, ahol a C++/WinRT-be már portolt metódus rendelkezik ezzel az aláírással.

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

Az adott metódus hívását az alábbihoz hasonló feladattá alakíthatja.

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

Vagy így.

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

Vegye figyelembe, hogy a lambdafüggvény Task visszatérési típusa explicit, mert a fordító nem tudja kikövetkeztetni.

A metódust egy ilyen tetszőleges tevékenységláncból is meghívhatjuk. Ismét egy explicit lambda visszatérési típussal.

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

winrt::IAsyncXxx<T> (a T egy Windows-futtatókörnyezet típus) átalakítása task<T^>-vá

Az ebben a szakaszban szereplő minta akkor érvényes, ha aszinkron módon ad vissza egy Windows-futtatókörnyezet értéket (a szemléltetéshez StorageFile-értéket használunk). Vegyünk egy példát, ahol a C++/WinRT-be már portolt metódus rendelkezik ezzel az aláírással.

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

Ez a következő lista bemutatja, hogyan konvertálhat egy hívást erre a metódusra feladattá. Vegye figyelembe, hogy meg kell hívnunk a to_cx interop segédfüggvényt, hogy a visszaadott C++/WinRT objektumot C++/CX fogantyúvá (más néven kalap) alakítsuk.

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

Íme ennek egy tömörebb verziója.

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

Akár úgy is dönthet, hogy ezt a mintát egy újrafelhasználható függvénysablonba burkolja, és return ugyanúgy, ahogy általában egy feladatot ad vissza.

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

Ha tetszik ez az ötlet, érdemes lehet a to_task elemet hozzáadni a interop_helpers.h-hoz.

Helyezze a(z) create_async elemet egy olyan feladat köré, amely a(z) co_return elemet használja.

Nem lehet co_return egy IAsyncXxx^ típust közvetlenül használni, de valami hasonlót el lehet érni. Ha van egy olyan feladat, amely együttműködve ad vissza egy értéket, akkor azt becsomagolhatja egy concurrency::create_async hívásba.

Íme egy hipotetikus példa, mivel nincs olyan példa, amit fel tudunk emelni a Simple3DGameDX-ről.

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

Mint látható, a visszatérési értéket bármilyen metódusból lekérheti co_await.

Port egyidejűsége::várakozás a következőre: co_await winrt::resume_after

A Simple3DGameDX néhány helyen a concurrency::wait függvényt használja a szál rövid időre történő szüneteltetésére. Íme egy példa.

// 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 concurrency::wait C++/WinRT-beli megfelelője a winrt::resume_after struktúra. Ez a szerkezet egy PPL-feladaton belül is használható co_await . Íme egy példa kódra.

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

Figyelje meg a két másik módosítást, amelyeket végre kellett hajtanunk. A GameConstants::InitialLoadingDelay típusátstd::chrono::d uration értékre módosítottuk, és explicitvá tettük a lambda függvény visszatérési típusát, mert a fordító már nem tudja levezetni.

Portolja át a(z) task<void> visszatérési típust a(z) winrt::IAsyncXxx-re

Simple3DGame::LoadLevelAsync

A Simple3DGameDX-szel végzett munkánk ezen szakaszában a projektben minden olyan hely, amely meghívja a Simple3DGame::LoadLevelAsync függvényt, a co_await használja annak meghívására.

Ez azt jelenti, hogy egyszerűen megváltoztathatjuk a metódus visszatérési típusát task<void>-ról winrt::Windows::Foundation::IAsyncAction-ra, miközben minden más változatlan marad.

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

Most már elég mechanikusnak kell lennie a módszer többi részének és függőségeinek (például m_level stb.) A C++/WinRT-be való portjának.

GameRenderer::LoadLevelResourcesAsync

A GameRenderer::LoadLevelResourcesAsync eredeti C++/CX verziója.

// 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 az egyetlen hely a projektben, amely meghívja a GameRenderer::LoadLevelResourcesAsync parancsot, és már használja co_await a hívását.

Így a GameRenderer::LoadLevelResourcesAsync függvénynek már nem kell feladatot visszaadnia — ehelyett winrt::Windows::Foundation::IAsyncAction típust is visszaadhat. Az implementáció maga pedig elég egyszerű ahhoz, hogy teljes egészében át lehessen ültetni C++/WinRT-re. Ez azt jelenti, hogy ugyanazt a módosítást kell elvégezni a co_await winrt::resume_after elemben, mint amit a Port concurrency::wait esetében végeztünk. És nincsenek jelentős függőségek a projekt többi részével kapcsolatban, amelyek miatt aggódnia kell.

Így néz ki a metódus, miután teljes egészében átültettük C++/WinRT-re.

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto LevelLoadingDelay = 500ms;
    ...
}

// GameRenderer.cpp
winrt::Windows::Foundation::IAsyncAction GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;
    co_return co_await winrt::resume_after(GameConstants::LevelLoadingDelay);
}

A cél – egy metódus teljes portja a C++/WinRT-be

Zárjuk le ezt az ismertetőt a végeredmény egy példájával, a BasicReaderWriter::ReadDataAsync metódus C++/WinRT-re való teljes átültetésével.

Amikor legutóbb megvizsgáltuk ezt a módszert (a Port ReadDataAsync (többnyire) C++/WinRT szakaszában, a projekt többi részét változatlanul hagyva), többnyire C++/WinRT-be lett portolva. De továbbra is egy Platform::Array<byte>^ típusú feladatot adott vissza.

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

Ahelyett, hogy egy Task objektumot adnánk vissza, úgy módosítjuk, hogy egy IAsyncOperation objektumot adjon vissza. És ahelyett, hogy egy bájttömböt adnánk vissza az IAsyncOperation használatával, ehelyett egy C++/WinRT IBuffer-objektumot adunk vissza. Ehhez kisebb módosításra is szükség lesz a hívási helyeken található kódban, amint azt látni fogjuk.

A metódus az implementációjának, a paraméterének és a m_location adattagnak a C++/WinRT szintaxis és objektumok használatára való átültetése után így néz ki.

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

Mint látható, a BasicReaderWriter::ReadDataAsync sokkal egyszerűbb, mivel a saját metódusába belefogalmaztuk a szinkron logikát, amely bájtokat kér le a pufferből.

Most azonban az ilyen struktúrából származó hívási helyeket kell portolnunk C++/CX-ben.

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

Ehhez a mintához a C++/WinRT-ben.

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

Fontos API-k