Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Návod
I když doporučujeme, abyste si toto téma přečetli od začátku, můžete rovnou přejít na souhrn technik vzájemné spolupráce v části Přehled přenosu asynchronní komunikace C++/CX do části C++/WinRT .
Toto je rozšířené téma týkající se postupného přenosu do C++/WinRT z C++/CX. Toto téma se zabývá tím, kde téma Spolupráce mezi C++/WinRT a C++/CX skončila.
Pokud velikost nebo složitost základu kódu vyžaduje postupné přenosy projektu, budete potřebovat proces přenosu, ve kterém po dobu, kdy kód C++/CX a C++/WinRT existuje vedle sebe ve stejném projektu. Pokud máte asynchronní kód, možná budete muset mít v projektu souběžné řetězy úloh a koroutiny knihovny PPL (Parallel Patterns Library), protože zdrojový kód budete postupně portovat. Toto téma se zaměřuje na techniky pro spolupráci mezi asynchronním kódem C++/CX a asynchronním kódem C++/WinRT. Tyto techniky můžete používat jednotlivě nebo společně. Tyto techniky vám umožňují provádět postupné, řízené lokální změny na cestě k převedení celého projektu, aniž by se každá změna nekontrolovaně promítla do celého projektu.
Než si přečtete toto téma, je vhodné si přečíst interoperabilitu mezi C++/WinRT a C++/CX. V tomto tématu se dozvíte, jak připravit váš projekt na postupné portování. Zavádí také dvě pomocné funkce, které můžete použít k převodu objektu C++/CX na objekt C++/WinRT (a naopak). Toto téma o asynchronii navazuje na tyto informace a využívá tyto pomocné funkce.
Note
Existují určitá omezení pro postupné přenosy z C++/CX do C++/WinRT. Pokud máte projekt komponenty prostředí Windows Runtime, postupný převod není možný a projekt budete muset převést najednou. A u projektu XAML musí být typy stránek XAML v daný okamžik buď všechny C++/WinRT nebo všechny C++/CX. Další informace najdete v tématu Přesunutí do C++/WinRT z C++/CX.
Důvod, proč je celé téma vyhrazené pro asynchronní interoperabilitu kódu
Přenos z C++/CX do C++/WinRT je obecně přímočarý, s jedinou výjimkou, a to při přechodu od úloh Parallel Patterns Library (PPL) ke korutinám. Modely se liší. Neexistuje přirozené mapování 1:1 z úkolů PPL na korutiny a neexistuje žádný jednoduchý způsob (který funguje ve všech případech) k mechanickému přenosu kódu.
Dobrou zprávou je, že převod úkolů na koroutiny má za následek významné zjednodušení. Vývojové týmy pravidelně uvádějí, že jakmile překonají úskalí spojená s portací svého asynchronního kódu, zbytek portace je z velké části mechanický.
Algoritmus byl často napsán tak, aby vyhovoval synchronním rozhraním API. A pak se to přeložilo na úkoly a explicitní pokračování – výsledkem často je neúmyslné obfuskace základní logiky. Například smyčky se stanou rekurzí; větve if-else se změní na vnořený strom (řetězec) úkolů; sdílené proměnné se stanou shared_ptr. Pokud chcete dekonstruovat často nepřirozenou strukturu zdrojového kódu PPL, doporučujeme, abyste se nejprve vrátili a porozuměli záměru původního kódu (tj. objevte původní synchronní verzi). A pak vložte co_await (kooperativní čekání) na příslušná místa.
Z tohoto důvodu platí, že pokud máte verzi asynchronního kódu v jazyce C# (spíše než C++/CX), ze které můžete při portování vycházet, může vám to usnadnit práci a vést k čistšímu převodu. Kód jazyka C# používá await. Takže kód v jazyce C# se už v podstatě řídí filozofií začít synchronní verzí a poté vložit await na příslušná místa.
Pokud nemáte verzi projektu v jazyce C#, můžete použít techniky popsané v tomto tématu. Jakmile portujete do C++/WinRT, bude struktura asynchronního kódu jednodušší portovat do jazyka C#, pokud chcete.
Některé pozadí asynchronního programování
Abychom měli společný výchozí rámec pro koncepty a terminologii asynchronního programování, pojďme si stručně nastínit asynchronní programování ve prostředí Windows Runtime obecně a také to, jak na něm obě jazykové projekce C++ každá svým způsobem staví.
Projekt obsahuje metody, které fungují asynchronně a existují dva hlavní typy.
- Před provedením něčeho jiného je běžné počkat na dokončení asynchronní práce. Metoda, která vrací asynchronní objekt operace je ten, na který můžete čekat.
- Někdy ale nechcete nebo potřebujete počkat na dokončení asynchronně provedené práce. V takovém případě je efektivnější, aby asynchronní metoda nevrací objekt asynchronní operace. Asynchronní metoda, jako je ta, na kterou nečekáte, se označuje jako metoda fire-and-forget .
Asynchronní objekty prostředí Windows Runtime (IAsyncXxx)
Obor názvů Windows::Foundation prostředí Windows Runtime obsahuje čtyři typy objektu asynchronní operace.
- IAsyncAction,
- IAsyncActionWithProgress<TProgress>,
- IAsyncOperation<TResult> a
- IAsyncOperationWithProgress<TResult, TProgress>.
V tomto tématu, když používáme pohodlnou zkratku IAsyncXxx, odkazujeme na tyto typy souhrnně; nebo mluvíme o jednom ze čtyř typů, aniž bychom museli určit, který z nich.
C++/CX async
Asynchronní kód C++/CX využívá úlohy knihovny PPL (Parallel Patterns Library). Úkol PPL je reprezentován třídou concurrency::task.
Asynchronní metoda C++/CX obvykle řetězí úlohy PPL pomocí funkcí lambda a concurrency::create_task a concurrency::task::then. Každá funkce lambda vrátí úlohu, která po dokončení vytvoří hodnotu, která se pak předá do lambda pokračování úkolu.
Alternativně místo volání create_task k vytvoření úlohy může asynchronní metoda C++/CX volat concurrency::create_async vytvořit IAsyncXxx^.
Návratový typ asynchronní metody C++/CX může být úloha PPL nebo IAsyncXxx^.
V obou případech metoda používá return klíčové slovo k vrácení asynchronního objektu, který po dokončení vytvoří hodnotu, kterou volající skutečně chce (například soubor, pole bajtů nebo logická hodnota).
Note
Pokud asynchronní metoda C++/CX vrátí IAsyncXxx^, je TResult (pokud existuje) omezena na typ prostředí Windows Runtime. Logická hodnota je například typ prostředí Windows Runtime, ale projektovaný typ C++/CX (například Platform::Array<byte>^) nikoli.
Asynchronní operace C++/WinRT
C++/WinRT integruje koroutiny C++ do programovacího modelu. Koroutiny a příkaz co_await poskytují přirozený způsob, jak kooperativně čekat na výsledek.
Každý z typů IAsyncXxx se projektuje do odpovídajícího typu v oboru názvů winrt::Windows::Foundation C++/WinRT. Označujme je jako winrt::IAsyncXxx (na rozdíl od IAsyncXxx^ z C++/CX).
Návratový typ korutiny C++/WinRT je buď winrt::IAsyncXxx, nebo winrt::fire_and_forget. A namísto použití klíčového slova return k vrácení asynchronního objektu používá korutina klíčové slovo co_return, aby kooperativně vrátila hodnotu, kterou volající skutečně chce (například soubor, pole bajtů nebo logickou hodnotu).
Pokud metoda obsahuje alespoň jeden příkaz co_await (nebo alespoň jeden co_return nebo co_yield), pak je metoda právě z tohoto důvodu korutinou.
Další informace a příklady kódu najdete v tématu Souběžnost a asynchronní operace s C++/WinRT.
Ukázka hry Direct3D (Simple3DGameDX)
Toto téma obsahuje návody k několika konkrétním programovacím technikám, které ukazují, jak postupně portovat asynchronní kód. Abychom mohli sloužit jako případová studie, použijeme verzi C++/CX ukázky hry Direct3D (která se nazývá Simple3DGameDX). Ukážeme si několik příkladů, jak v projektu vzít původní zdrojový kód C++/CX a postupně přenést jeho asynchronní kód do C++/WinRT.
- Stáhněte si ZIP z výše uvedeného odkazu a rozbalte ho.
- Otevřete projekt C++/CX (je ve složce s názvem
cpp) v Visual Studio. - Pak budete muset do projektu přidat podporu C++/WinRT. Postup, jak to provést, je popsán v tématu Převzetí projektu C++/CX a přidání podpory C++/WinRT. V této části je krok týkající se přidání hlavičkového souboru
interop_helpers.hdo projektu obzvlášť důležitý, protože v tomto tématu budeme spoléhat na tyto pomocné funkce. - Nakonec přidejte
#include <pplawait.h>dopch.h. To vám poskytne podporu PPL (další informace o této podpoře najdete v následující části).
Zatím nevytvořijte, jinak se zobrazí chyby týkající se nejednoznačnosti bajtů . Tady je postup, jak to vyřešit.
- Otevřete
BasicLoader.cppa zakomentujteusing namespace std;. - Ve stejném souboru zdrojového kódu budete muset kvalifikovat shared_ptr jako std::shared_ptr. Můžete to udělat pomocí hledání a nahrazení v rámci daného souboru.
- Potom kvalifikujte vektor jako std::vector a řetězec jako std::string.
Projekt se teď znovu sestaví, má podporu C++/WinRT a obsahuje pomocné funkce pro interoperabilitu from_cx a to_cx.
Nyní máte projekt Simple3DGameDX připravený, abyste mohli postupovat podle podrobného výkladu kódu v tomto tématu.
Přehled přenosu synchronizace C++/CX do C++/WinRT
Stručně řečeno, při portování budeme měnit řetězce úloh PPL na volání co_await. Změníme návratovou hodnotu metody z úlohy PPL na objekt C++/ WinRT winrt::IAsyncXxx . A také změníme všechny IAsyncXxx^ na C++/WinRT winrt::IAsyncXxx.
Pamatujte, že korutina je jakákoli metoda, která volá co_xxx. Korutina C++/WinRT používá co_return ke kooperativnímu vrácení své hodnoty. Díky podpoře pro coroutine v PPL (díky pplawait.h) můžete také použít co_return k vrácení úlohy PPL z coroutine. A můžete také co_await jak úlohy, tak IAsyncXxx. Nemůžete ale použít co_return pro vrácení IAsyncXxx^. Následující tabulka popisuje podporu interoperability mezi různými asynchronními technikami znázorněnými na obrázku pplawait.h.
| Metoda | Můžeš co_await to? |
Můžeš z toho co_return? |
|---|---|---|
| Metoda vrací úlohu<void> | Yes | Yes |
| Metoda vrátí úlohu<T>. | No | Yes |
| Metoda vrátí IAsyncXxx.^ | Yes | Ne. Ale úlohu, která používá co_return, obalíte pomocí create_async. |
| Metoda vrátí winrt::IAsyncXxx. | Yes | Yes |
Pomocí této další tabulky můžete přejít přímo do oddílu v tomto tématu, který popisuje techniku spolupráce, která je zajímavá, nebo pokračujte v čtení odsud.
| Asynchronní technika spolupráce | Oddíl v tomto tématu |
|---|---|
Pomocí co_await lze čekat na metodu task<void> uvnitř metody typu fire-and-forget nebo v rámci konstruktoru. |
await úkolu<void> v metodě fire-and-forget |
Použijte co_await k čekání na metodu task<void> uvnitř metody task<void>. |
Použijte task<void> v metodě task<void> |
Pomocí co_await můžete čekat na metodu Task<void> v rámci metody Task<T>. |
Await v rámci úlohy<void> v metodě úlohy<T> |
Pomocí co_await lze čekat na metodu IAsyncXxx^. |
Čeká na IAsyncXxx^ v metodě úkolu a zbytek projektu zůstane beze změny. |
Použijte co_return v metodě task<void>. |
Použití Await uvnitř metody Task<void> v metodě Task<void> |
Použijte co_return v metodě task<T>. |
Počkat na IAsyncXxx^ v metodě task a beze změny ponechat zbytek projektu |
Zabalte create_async kolem úkolu, který používá co_return. |
Obalte úlohu, která používá co_return, pomocí create_async |
| Přenést concurrency::wait. |
Přenést concurrency::wait do co_await winrt::resume_after |
| Vrátit winrt::IAsyncXxx místo task<void>. | Přeneste návratový typ task<void> na winrt::IAsyncXxx |
| Převeďte winrt::IAsyncXxx<T> (T je primitivní) na úlohu<T>. | Převod winrt::IAsyncXxx<T> (T je primitivní) na úlohu<T> |
| Převeďte winrt::IAsyncXxx<T> (T je typ prostředí Windows Runtime) na úlohu<T^>. | Převod winrt::IAsyncXxx<T> (T je typ prostředí Windows Runtime) na úlohu<T^> |
Tady je krátký příklad kódu, který ilustruje některé z podpory.
#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();
}
Důležité
I s těmito skvělými možnostmi interoperability závisí postupné portování na volbě změn, které můžeme provádět cíleně, aniž by ovlivnily zbytek projektu. Chceme se vyhnout tomu, abychom zatáhli za nějaký volný konec a tím nerozpletli strukturu celého projektu. Pro to musíme dělat věci v určitém pořadí. Dále se podrobněji podíváme na několik příkladů změn souvisejících s portováním a interoperabilitou v oblasti asynchronních operací.
Vyčkejte na metodu task<void>, zbytek projektu ponechte beze změny
Metoda, která vrací task<void>, provádí práci asynchronně a vrací objekt asynchronní operace, ale ve výsledku nevrací žádnou hodnotu.
co_await Můžeme takhle použít metodu.
Takže dobré místo, kde začít s postupným převáděním asynchronního kódu, je najít místa, kde takové metody voláte. Tato místa budou obnášet vytvoření a/nebo vrácení úkolu. Mohou také zahrnovat typ řetězce úloh, kdy se z každé úlohy jejímu pokračování nepředává žádná hodnota. Na takových místech stačí asynchronní kód nahradit příkazy co_await, jak uvidíme.
Note
V průběhu tohoto tématu uvidíte výhodu této strategie. Jakmile je konkrétní metoda task<void> volána výhradně prostřednictvím co_await, můžete ji poté přenést do C++/WinRT a nechat ji vracet winrt::IAsyncXxx.
Pojďme najít několik příkladů. Otevřete projekt Simple3DGameDX (viz ukázka hry Direct3D).
Důležité
V následujících příkladech, jak vidíte změny implementace metod, mějte na paměti, že není nutné měnit volající metody, které měníme. Tyto změny jsou lokalizované a neprovedou se kaskádově přes projekt.
Awaitování Task<void> v metodě fire-and-forget
Začněme s použitím await u task<void> v metodách fire-and-forget, protože to je ten nejjednodušší případ. Jde o metody, které pracují asynchronně, ale volající nečeká, až se tato práce dokončí. Metodu jednoduše zavoláte a dál se o ni nestaráte, navzdory tomu, že se dokončí asynchronně.
Zaměřte se na kořen grafu závislostí svého projektu a vyhledejte metody void, které obsahují create_task, a/nebo řetězení úloh, v nichž jsou volány pouze metody task<void>.
V Simple3DGameDX najdete kód podobný tomu v implementaci metody GameMain::Update. Je v souboru GameMain.cppzdrojového kódu .
GameMain::Update
Tady je extrakce z verze C++/CX metody, která znázorňuje dvě části metody, které jsou dokončeny asynchronně.
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());
...
...
}
Můžete vidět volání metody Simple3DGame::LoadLevelAsync (která vrací PPL task<void>). Poté následuje pokračování, které provádí nějakou synchronní práci. LoadLevelAsync je asynchronní, ale nevrací hodnotu. Do pokračování se tedy z úkolu nepředává žádná hodnota.
Stejný druh změny kódu můžeme provést na těchto dvou místech. Kód je vysvětlen po níže uvedeném výpisu. Mohli bychom zde diskutovat o bezpečném přístupu k tomuto ukazateli v korutině člena třídy. Pojďme to ale odložit pro pozdější část (odložená diskuze co_await o tomto ukazateli) – prozatím tento kód funguje.
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;
...
...
}
Jak vidíte, protože LoadLevelAsync vrací úlohu, můžeme co_await ji použít. A nemusíme explicitně definovat pokračování – kód, který následuje po co_await, se provede až po dokončení LoadLevelAsync.
Přidání co_await změní metodu na korutinu, takže jsme ji nemohli ponechat, aby vracela void. Jedná se o metodu fire-and-forget, takže jsme ji změnili tak, aby vrátila winrt::fire_and_forget.
Budete také muset upravit GameMain.h. Změňte návratový typ GameMain::Update z void na winrt::fire_and_forget v deklaraci.
Tuto změnu můžete provést ve své kopii projektu a hra se bude i nadále sestavovat a poběží stejně. Zdrojový kód je stále v podstatě C++/CX, ale teď používá stejné vzory jako C++/WinRT, takže jsme se trochu blíž dostali k tomu, abychom mohli zbytek kódu přenést mechanicky.
GameMain::ResetGame
GameMain::ResetGame je další metoda typu „fire-and-forget“; rovněž volá LoadLevelAsync. Takže tam můžete provést stejnou změnu v kódu, pokud si to chcete procvičit.
GameMain::OnDeviceRestored
Věci jsou v GameMain::OnDeviceRestored zajímavé kvůli hlubšímu vnoření asynchronního kódu, včetně úlohy no-op. Tady je přehled asynchronních částí metody (s méně zajímavým synchronním kódem reprezentovaným třemi tečkami).
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());
}
Nejprve změňte návratový typ GameMain::OnDeviceRestored z void na winrt::fire_and_forget in GameMain.h a .cpp. Budete také muset otevřít DeviceResources.h a provést stejnou změnu návratového typu IDeviceNotify::OnDeviceRestored.
Chcete-li převést asynchronní kód, odeberte všechna volání create_task a then i jejich složené závorky a zjednodušte metodu na plochou posloupnost příkazů.
Změňte jakýkoli return, který vrací objekt Task, na co_await. Zůstane vám jedno return, které nic nevrací, tak ho prostě smažte. Jakmile budete hotovi, úloha no-op zmizí a osnova asynchronních částí metody bude vypadat takto. Opět platí, že méně zajímavý synchronní kód je elidován.
winrt::fire_and_forget GameMain::OnDeviceRestored()
{
...
co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
...
if (m_updateState == UpdateEngineState::WaitingForResources)
{
co_await m_game->LoadLevelAsync();
...
}
...
}
Jak vidíte, tato forma asynchronní struktury je výrazně jednodušší a čitelnější.
GameMain::GameMain
Konstruktor GameMain::GameMain provádí práci asynchronně a žádná část projektu nečeká na dokončení této práce. Tento výpis znovu popisuje asynchronní části.
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());
}
Konstruktor ale nemůže vracet winrt::fire_and_forget, takže asynchronní kód přesuneme do nové metody typu fire-and-forget GameMain::ConstructInBackground, převedeme kód na posloupnost příkazů co_await a novou metodu zavoláme z konstruktoru. Tady je výsledek.
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();
...
}
...
}
Nyní byly všechny metody typu fire-and-forget — vlastně veškerý asynchronní kód — v GameMain převedeny na korutiny. Pokud budete chtít, mohli byste se podívat po metodách typu „spusť a zapomeň“ v jiných třídách a provést podobné úpravy.
Odložená diskuze o co_awaittomto ukazateli
Když jsme provedli změny GameMain::Update, odložil jsem diskuzi o tomto ukazateli. Pojďme o tom diskutovat tady.
To platí pro všechny metody, které jsme zatím změnili; a platí to pro všechny korutiny, nejen ty typu fire-and-forget. Vložení co_await do metody vytvoří bod přerušení. A proto musíme být opatrní s tímto ukazatelem, který samozřejmě používáme po bodu pozastavení pokaždé, když přistupujeme k členu třídy.
Krátký příběh spočívá v tom, že řešením je volání implements::get_strong. Kompletní diskuzi o problému a řešení najdete v tématu Bezpečný přístup k tomuto ukazateli v korutině člena třídy.
Můžete volat implements::get_strong pouze ve třídě, která je odvozena z winrt::implements.
Odvození GameMain z winrt::implements
První změna, která musíme udělat, je v GameMain.h.
class GameMain :
public DX::IDeviceNotify
GameMain bude nadále implementovat DX::IDeviceNotify, ale změníme ji tak, aby byla odvozena z winrt::implements.
class GameMain :
public winrt::implements<GameMain, winrt::Windows::Foundation::IInspectable>,
DX::IDeviceNotify
V dalším kroku App.cppnajdete tuto metodu.
void App::Load(Platform::String^)
{
if (!m_main)
{
m_main = std::unique_ptr<GameMain>(new GameMain(m_deviceResources));
}
}
Ale teď, když GameMain pochází z winrt::implements, musíme ho vytvořit jiným způsobem. V tomto případě použijeme šablonu funkce winrt::make_self . Další informace najdete v tématu Vytváření instancí a vracení typů implementace a rozhraní.
Nahraďte tento řádek kódu tímto kódem.
...
m_main = winrt::make_self<GameMain>(m_deviceResources);
...
Abychom tuto změnu dotáhli do konce, budeme také muset změnit typ m_main. V App.h najdete tento kód.
ref class App sealed :
public Windows::ApplicationModel::Core::IFrameworkView
{
...
private:
...
std::unique_ptr<GameMain> m_main;
};
Změňte tuto deklaraci m_main na tuto.
...
winrt::com_ptr<GameMain> m_main;
...
Teď můžeme volat implements::get_strong
U GameMain::Update i u všech dalších metod, do kterých jsme přidali co_await, lze na začátku korutiny zavolat get_strong, aby bylo zajištěno, že silná reference přetrvá až do dokončení korutiny.
winrt::fire_and_forget GameMain::Update()
{
auto strong_this{ get_strong() }; // Keep *this* alive.
...
co_await ...
...
}
`await` task<void> v metodě task<void>
Dalším nejjednodušším případem je očekávání task<void> uvnitř metody, která sama vrací task<void>. To je proto, že můžeme co_awaittask<void>, a můžeme co_return z některého z nich.
Velmi jednoduchý příklad najdete v implementaci metody Simple3DGame::LoadLevelAsync. Je v souboru Simple3DGame.cppzdrojového kódu .
task<void> Simple3DGame::LoadLevelAsync()
{
m_level[m_currentLevel]->Initialize(m_objects);
m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
return m_renderer->LoadLevelResourcesAsync();
}
Existuje jen nějaký synchronní kód, za kterým následuje vrácení úlohy vytvořené GameRenderer::LoadLevelResourcesAsync.
Místo toho, abychom tento úkol vrátili co_await , a pak co_return výsledek 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();
}
To nevypadá jako hlubokou změnu. Ale teď, když voláme GameRenderer::LoadLevelResourcesAsync přes co_await, můžeme ji převést tak, aby místo typu task vracela winrt::IAsyncXxx. Tomu se budeme věnovat později v části Převod návratového typu task<void> na winrt::IAsyncXxx.
Použijte await u task<void> v metodě task<T>
I když v Simple3DGameDX nejsou k dispozici žádné vhodné příklady, můžeme vytvořit hypotetický příklad, abychom ukázali vzor.
První řádek v níže uvedeném příkladu kódu ukazuje jednoduché co_await použití task<void>. Potom, aby bylo možné splnit požadavky návratového typu task<T>, musíme asynchronně vrátit objekt typu StorageFile^. Uděláme to tak, že co_await rozhraní API systému prostředí Windows Runtime a co_return výsledný soubor.
task<StorageFile^> Simple3DGame::LoadLevelAndRetrieveFileAsync(
StorageFolder^ location,
Platform::String^ filename)
{
co_await m_renderer->LoadLevelResourcesAsync();
co_return co_await location->GetFileAsync(filename);
}
Do C++/WinRT bychom mohli dokonce přenést další metodu.
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);
}
Datový člen m_renderer je v tomto příkladu stále C++/CX.
Čekání na IAsyncXxx^ v metodě task při ponechání zbytku projektu beze změny
Viděli jsme, jak můžete co_awaitúlohu<zrušit>. Můžete také co_await použít metodu, která vrací IAsyncXxx, ať už jde o metodu ve vašem projektu, nebo asynchronní rozhraní API Windows (například StorageFolder.GetFileAsync, které jsme spolu očekávali v předchozí části).
Příklad, kdy můžeme tento druh změny kódu provést, se podíváme na BasicReaderWriter::ReadDataAsync (najdete ji implementovanou v BasicReaderWriter.cpp).
Tady je původní verze C++/CX.
task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
_In_ Platform::String^ filename
)
{
return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
{
return FileIO::ReadBufferAsync(file);
}).then([=](IBuffer^ buffer)
{
auto fileData = ref new Platform::Array<byte>(buffer->Length);
DataReader::FromBuffer(buffer)->ReadBytes(fileData);
return fileData;
});
}
Následující výpis kódu ukazuje, že můžeme co_await rozhraní API systému Windows, která vracejí IAsyncXxx^. Nejen to, můžeme také co_return asynchronně získat hodnotu, kterou BasicReaderWriter::ReadDataAsync vrací (v tomto případě pole bajtů). Tento první krok ukazuje, jak provést pouze tyto změny; Kód C++/CX ve skutečnosti portujeme do C++/WinRT v další části.
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;
}
Opět nemusíme měnit volající metody, které měníme, protože jsme nezměnili návratový typ.
Port ReadDataAsync (většinou) do C++/WinRT a zbytek projektu zůstane beze změny.
Můžeme přejít k dalšímu kroku a přenést metodu téměř úplně do C++/WinRT, aniž bychom museli měnit jakoukoli jinou část projektu.
Jedinou závislostí, kterou tato metoda má ve zbytku projektu, je BasicReaderWriter::m_location datový člen, což je C++/CX StorageFolder^. Pokud chcete tento datový člen ponechat beze změny a nechat typ parametru a návratový typ beze změny, potřebujeme provést pouze několik převodů – jeden na začátku metody a druhý na konci. K tomu můžeme použít pomocné funkce pro interoperabilitu from_cx a to_cx.
Takhle vypadá BasicReaderWriter::ReadDataAsync po převedení jeho implementace převážně do C++/WinRT. Jedná se o dobrý příklad postupného přenosu. A tato metoda je ve fázi, kdy se můžeme přesunout od myšlení jako metody C++/CX, která používá některé techniky C++/WinRT, a podívat se na ni jako na metodu C++/WinRT, která spolupracuje s 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
V souboru ReadDataAsync výše vytvoříme a vrátíme nové pole C++/CX. A samozřejmě to děláme, abychom splnili návratový typ metody (takže nemusíme měnit zbytek projektu).
Můžete se setkat s dalšími příklady ve vlastním projektu, kde po přenosu dosáhnete konce metody a vše, co máte, je objekt C++/WinRT. Stačí co_return zavolat to_cx a převést ho. Další informace o tom a příklad najdete v následující části.
Převod winrt::IAsyncXxx<T> na úlohu<T>
Tato část se zabývá situací, kdy jste portovali asynchronní metodu do C++/WinRT (aby vrátila winrt::IAsyncXxx<T>), ale stále máte kód C++/CX volající tuto metodu, jako by stále vracela úlohu.
- Jedním z případů je, že T je primitivní, což nevyžaduje převod.
- Druhým případem je, že T je typ prostředí Windows Runtime, v takovém případě ho budete muset převést na typ T^.
Převod winrt::IAsyncXxx<T> (T je primitivní) na úlohu<T>
Vzor v této části platí, když asynchronně vracíte primitivní hodnotu (k ilustraci použijeme logickou hodnotu). Představte si příklad, kdy metoda, kterou jste už portovali do C++/WinRT, má tento podpis.
winrt::Windows::Foundation::IAsyncOperation<bool>
MyClass::GetBoolMemberFunctionAsync()
{
bool value = ...
co_return value;
}
Volání této metody můžete převést na úlohu, jako je tato.
task<bool> MyClass::RetrieveBoolTask()
{
co_return co_await GetBoolMemberFunctionAsync();
}
Nebo takhle.
task<bool> MyClass::RetrieveBoolTask()
{
return concurrency::create_task(
[this]() -> concurrency::task<bool> {
auto result = co_await GetBoolMemberFunctionAsync();
co_return result;
});
}
Všimněte si, že návratový typ úlohy funkce lambda je explicitní, protože kompilátor jej nemůže odvodit.
Metodu bychom také mohli volat z libovolného řetězu úkolů, jako je tento. Opět s explicitním návratovým typem lambda.
...
.then([this]() -> concurrency::task<bool> {
co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
...
});
...
Převod winrt::IAsyncXxx<T> (T je typ prostředí Windows Runtime) na úlohu<T^>
Vzor v této části platí, když asynchronně vracíte hodnotu prostředí Windows Runtime (k ilustraci použijeme hodnotu StorageFile). Představte si příklad, kdy metoda, kterou jste už portovali do C++/WinRT, má tento podpis.
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
MyClass::GetStorageFileMemberFunctionAsync()
{
co_return co_await winrt::Windows::Storage::StorageFile::GetFileFromPathAsync
(L"MyFile.txt");
}
Tento další výpis ukazuje, jak převést volání této metody na úkol. Všimněte si, že musíme zavolat pomocnou funkci pro interoperabilitu to_cx, aby převedla vrácený objekt C++/WinRT na objekt typu handle v C++/CX (označovaný také jako hat).
task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
winrt::Windows::Storage::StorageFile storageFile =
co_await GetStorageFileMemberFunctionAsync();
co_return to_cx<Windows::Storage::StorageFile>(storageFile);
}
Tady je stručnější verze.
task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
co_return to_cx<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}
A dokonce se můžete rozhodnout zapouzdřit tento vzor do opakovaně použitelné šablony funkce a return vrátit ji stejně, jako byste normálně vraceli 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());
}
Pokud se vám tento nápad líbí, můžete přidat to_task do interop_helpers.h.
Zabalení create_async kolem úlohy, která používá co_return
co_return Instanci IAsyncXxx^ nemůžete vytvořit přímo, ale můžete dosáhnout podobného výsledku. Pokud máte úlohu, která vrací hodnotu, můžete ji zapouzdřit do volání concurrency::create_async.
Tady je hypotetický příklad, protože neexistuje příklad, který bychom mohli z Simple3DGameDX zvednout.
Windows::Foundation::IAsyncOperation<bool>^ MyClass::RetrieveBoolAsync()
{
return concurrency::create_async(
[this]() -> concurrency::task<bool> {
bool result = co_await GetBoolMemberFunctionAsync();
co_return result;
});
}
Jak vidíte, můžete získat vrácenou hodnotu z jakékoli metody, kterou můžete co_await.
Přenést concurrency::wait do co_await winrt::resume_after
Na několika místech Simple3DGameDX používá concurrency::wait k pozastavení vlákna na krátkou dobu. Tady je příklad.
// 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);
}));
...
}
Verzí concurrency::wait v C++/WinRT je struktura winrt::resume_after. Tuto strukturu můžeme co_await provést uvnitř úkolu PPL. Tady je příklad kódu.
// 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);
}));
...
}
Všimněte si dvou dalších změn, které jsme museli udělat. Změnili jsme typ GameConstants::InitialLoadingDelay na std::chrono::d uration a vrátili jsme typ funkce lambda explicitně, protože kompilátor už ho nemůže odvodit.
Přenést návratový typ task<void> do winrt::IAsyncXxx
Simple3DGame::LoadLevelAsync
V této fázi naší práce s Simple3DGameDX všechna místa v projektu, která volají Simple3DGame::LoadLevelAsync, k tomu používají co_await.
To znamená, že můžeme jednoduše změnit návratový typ této metody z objektu void< úkolu> na winrt::Windows::Foundation::IAsyncAction (zbytek metody zůstane beze změny).
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();
}
Teď by mělo být poměrně mechanické přenést zbytek této metody a jeho závislosti (například m_level atd.) do C++/WinRT.
GameRenderer::LoadLevelResourcesAsync
Tady je původní verze C++/CX 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 je jediným místem v projektu, který volá GameRenderer::LoadLevelResourcesAsync a už ho používá co_await k volání.
Proto už není potřeba, aby GameRenderer::LoadLevelResourcesAsync vrátil úlohu – může vrátit winrt::Windows::Foundation::IAsyncAction. A samotná implementace je dostatečně jednoduchá pro kompletní port do C++/WinRT. To zahrnuje provedení stejné změny, jakou jsme provedli v Port concurrency::wait to co_await winrt::resume_after. A ve zbytku projektu se nemusíte starat o žádné významné závislosti.
Tady je postup, jak metoda vypadá po jeho úplném přenosu do 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);
}
Cílem – plně portovat metodu do C++/WinRT
Pojďme tuto ukázku uzavřít příkladem konečného výsledku, a to úplným převedením metody BasicReaderWriter::ReadDataAsync na C++/WinRT.
Když jsme se naposledy podívali na tuto metodu (v části Port ReadDataAsync (většinou) do C++/WinRT, zbytek projektu zůstal beze změny), byl většinou portován do C++/WinRT. Přesto ale vrátil úkol typu 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());
}
Místo vrácení úkolu ji změníme tak, aby vrátila operaci IAsyncOperation. Místo vrácení pole bajtů prostřednictvím této operace IAsyncOperation místo toho vrátíme objekt C++/WinRT IBuffer . To bude také vyžadovat menší změnu v kódu v místech volání, jak uvidíme.
Takto metoda vypadá po převedení její implementace, jejího parametru a datového členu m_location tak, aby používaly syntaxi a objekty C++/WinRT.
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::Streams::IBuffer>
BasicReaderWriter::ReadDataAsync(
_In_ winrt::hstring const& filename)
{
StorageFile file{ co_await m_location.GetFileAsync(filename) };
co_return co_await FileIO::ReadBufferAsync(file);
}
winrt::array_view<byte> BasicLoader::GetBufferView(
winrt::Windows::Storage::Streams::IBuffer const& buffer)
{
byte* bytes;
auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
winrt::check_hresult(byteAccess->Buffer(&bytes));
return { bytes, bytes + buffer.Length() };
}
Jak vidíte, BasicReaderWriter::ReadDataAsync samotný je mnohem jednodušší, protože jsme vzali do vlastní metody synchronní logiku, která načítá bajty z vyrovnávací paměti.
Teď ale potřebujeme převést místa volání z tohoto typu struktury v C++/CX.
task<void> BasicLoader::LoadTextureAsync(...)
{
return m_basicReaderWriter->ReadDataAsync(filename).then(
[=](const Platform::Array<byte>^ textureData)
{
CreateTexture(...);
});
}
K tomuto vzoru v C++/WinRT.
winrt::Windows::Foundation::IAsyncAction BasicLoader::LoadTextureAsync(...)
{
auto textureBuffer = co_await m_basicReaderWriter.ReadDataAsync(filename);
auto textureData = GetBufferView(textureBuffer);
CreateTexture(...);
}
Důležitá rozhraní API
- IAsyncAction
- IAsyncActionWithProgress<TProgress>
- IAsyncOperation<TResult>
- IAsyncOperationWithProgress<TResult, TProgress>
- implements::get_strong
- concurrency::create_async
- souběžnost::create_task
- concurrency::task
- concurrency::task::then
- concurrency::wait
- winrt::fire_and_forget
- winrt::make_self
Příbuzná témata
Windows developer