Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Tip
Bien que nous vous recommandons de lire cette rubrique à partir du début, vous pouvez passer directement à un résumé des techniques d’interopérabilité dans la vue d’ensemble du portage asynchrone C++/CX vers C++/WinRT .
Il s’agit d’une rubrique avancée liée au portage progressif vers C++/WinRT à partir de C++/CX. Cette rubrique fait suite à la rubrique Interopérabilité entre C++/WinRT et C++/CX.
Si la taille ou la complexité de votre codebase rend nécessaire le portage progressif de votre projet, vous aurez besoin d’un processus de portage dans lequel le code C++/CX et C++/WinRT existe côte à côte dans le même projet. Si vous avez du code asynchrone, vous devrez peut-être avoir des chaînes de tâches PPL (Parallel Patterns Library) et des coroutines côte à côte dans votre projet, car vous portez progressivement votre code source. Cette rubrique se concentre sur les techniques d’interopérabilité entre le code C++/CX asynchrone et le code C++/WinRT asynchrone. Vous pouvez utiliser ces techniques individuellement ou ensemble. Les techniques vous permettent d’apporter des modifications progressives, contrôlées et locales dans le processus de portage de l’ensemble de votre projet, sans que chaque modification n’entraîne des répercussions incontrôlées dans tout le projet.
Avant de lire cette rubrique, il est judicieux de lire l’interopérabilité entre C++/WinRT et C++/CX. Cette rubrique vous montre comment préparer votre projet pour le portage progressif. Il introduit également deux fonctions d’assistance que vous pouvez utiliser pour convertir un objet C++/CX en objet C++/WinRT (et vice versa). Cette rubrique sur l’asynchronie s’appuie sur ces informations et utilise ces fonctions d’assistance.
Note
Il existe certaines limitations pour le portage progressif de C++/CX vers C++/WinRT. Si vous avez un projet de composant Windows Runtime, le portage progressif n'est pas possible, et vous devez porter le projet en une seule passe. Et pour un projet XAML, à tout moment, vos types de pages XAML doivent être tous C++/WinRT ou C++/CX. Pour plus d’informations, consultez la rubrique Déplacer vers C++/WinRT à partir de C++/CX.
La raison pour laquelle une rubrique entière est dédiée à l’interopérabilité de code asynchrone
Le portage de C++/CX vers C++/WinRT est généralement simple, à l’exception du passage des tâches PPL (Parallel Patterns Library) aux coroutines. Les modèles sont différents. Il n’existe pas de mappage un-à-un naturel des tâches PPL aux coroutines, et il n’existe aucun moyen simple (qui fonctionne pour tous les cas) de porter mécaniquement le code.
La bonne nouvelle, c’est que la conversion des tâches en coroutines entraîne des simplifications significatives. Et les équipes de développement signalent régulièrement qu’une fois qu’elles sont sur le point de porter leur code asynchrone, le reste du travail de portage est en grande partie mécanique.
Souvent, un algorithme a été écrit à l’origine pour répondre aux API synchrones. Ensuite, cela a été traduit en tâches et continuations explicites , le résultat étant souvent une obfuscation accidentelle de la logique sous-jacente. Par exemple, les boucles deviennent récursivité ; les branches if-else se transforment en une arborescence imbriquée (une chaîne) de tâches ; les variables partagées deviennent shared_ptr. Pour déconstructer la structure souvent non naturelle du code source PPL, nous vous recommandons de revenir en arrière et de comprendre l’intention du code d’origine (autrement dit, découvrir la version synchrone d’origine). Ensuite, insérez co_await (attendez coopérativement) dans les endroits appropriés.
Pour cette raison, si vous disposez d’une version C# (plutôt que C++/CX) du code asynchrone à partir de laquelle commencer votre port, cela peut vous donner un temps plus facile et un port plus propre. Le code C# utilise await. Ainsi, le code C# suit déjà une philosophie de commencer par une version synchrone, puis d’insérer await dans les emplacements appropriés.
Si vous n’avez pas de version C# de votre projet, vous pouvez utiliser les techniques décrites dans cette rubrique. Et une fois que vous avez porté vers C++/WinRT, la structure de votre code asynchrone sera ensuite plus facile à porter vers C#, si vous souhaitez.
Quelques notions de programmation asynchrone
Pour que nous disposions d'un cadre de référence commun pour les concepts de programmation asynchrones et la terminologie, nous allons brièvement définir la scène concernant Windows Runtime programmation asynchrone en général, ainsi que la façon dont les deux projections de langage C++ sont chacune, de leur manière différente, superposées en plus de cela.
Votre projet a des méthodes qui fonctionnent de manière asynchrone et il existe deux types principaux.
- Il est courant d’attendre la fin du travail asynchrone avant de faire autre chose. Une méthode qui renvoie un objet d’opération asynchrone est une méthode sur laquelle on peut effectuer une attente.
- Mais parfois, vous ne souhaitez pas ou n’avez pas besoin d’attendre la fin du travail effectué de manière asynchrone. Dans ce cas, il est plus efficace pour la méthode asynchrone de ne pas retourner un objet d’opération asynchrone. Une méthode asynchrone telle que celle que vous n’attendez pas est connue sous le nom de méthode fire-and-forget .
objets asynchrones du Runtime Windows (IAsyncXxx)
L’espace de noms Windows Runtime Windows ::Foundation contient quatre types d’objet d’opération asynchrone.
- IAsyncAction,
- IAsyncActionWithProgress<TProgress>,
- IAsyncOperation<TResult>, et
- IAsyncOperationWithProgress<TResult, TProgress>.
Dans cette rubrique, lorsque nous utilisons le raccourci pratique d’IAsyncXxx, nous faisons référence collectivement à ces types ; ou nous parlons de l’un des quatre types sans avoir à spécifier celui-ci.
Synchronisation asynchrone C++/CX
Le code C++/CX asynchrone utilise des tâches PPL (Parallel Patterns Library). Une tâche PPL est représentée par la classe concurrency ::task .
En règle générale, une méthode C++/CX asynchrone associe des tâches PPL à l’aide de fonctions lambda avec concurrency ::create_task et concurrency ::task ::then. Chaque fonction lambda retourne une tâche qui, lorsqu’elle se termine, produit une valeur qui est ensuite passée dans l’lambda de la continuation de la tâche.
Au lieu d’appeler create_task pour créer une tâche, une méthode C++/CX asynchrone peut appeler concurrency ::create_async pour créer un IAsyncXxx^.
Par conséquent, le type de retour d’une méthode C++/CX asynchrone peut être une tâche PPL ou un IAsyncXxx^.
Dans les deux cas, la méthode elle-même utilise le return mot clé pour retourner un objet asynchrone qui, lorsqu’il se termine, produit la valeur souhaitée par l’appelant (peut-être un fichier, un tableau d’octets ou une valeur booléenne).
Note
Si une méthode C++/CX asynchrone retourne un IAsyncXxx^, le TResult (le cas échéant) est limité à être un type Windows Runtime. Une valeur booléenne, par exemple, est un type Windows Runtime ; mais ce n’est pas le cas d’un type projeté en C++/CX (par exemple, Platform::Array<byte>^).
C++/WinRT asynchrone
C++/WinRT intègre les coroutines C++ dans le modèle de programmation. Coroutines et l’instruction co_await fournissent un moyen naturel d’attendre coopérativement un résultat.
Chacun des types IAsyncXxx est projeté dans un type correspondant dans l’espace de noms winrt ::Windows ::Foundation C++/WinRT. Appelons ceux-ci winrt::IAsyncXxx (par opposition au IAsyncXxx^ de C++/CX).
Le type de retour d’une coroutine C++/WinRT est soit winrt ::IAsyncXxx, soit winrt ::fire_and_forget. Et au lieu d’utiliser le return mot clé pour retourner un objet asynchrone, une coroutine utilise le co_return mot clé pour retourner de manière coopérative la valeur souhaitée par l’appelant (peut-être un fichier, un tableau d’octets ou une valeur booléenne).
Si une méthode contient au moins une co_await instruction (ou au moins une co_return ou co_yield), la méthode est une coroutine pour cette raison.
Pour plus d’informations et des exemples de code, consultez Accès concurrentiel et opérations asynchrones avec C++/WinRT.
Exemple de jeu Direct3D (Simple3DGameDX)
Cette rubrique contient des procédures pas à pas de plusieurs techniques de programmation spécifiques qui illustrent comment porter progressivement du code asynchrone. Pour servir d’étude de cas, nous allons utiliser la version C++/CX de l’exemple de jeu Direct3D (qui est appelé Simple3DGameDX). Nous allons montrer quelques exemples de la façon dont vous pouvez prendre le code source C++/CX d’origine dans ce projet et porter progressivement son code asynchrone vers C++/WinRT.
- Téléchargez le fichier ZIP à partir du lien ci-dessus et décompressez-le.
- Ouvrez le projet C++/CX (il se trouve dans le dossier nommé
cpp) dans Visual Studio. - Vous devez ensuite ajouter la prise en charge de C++/WinRT au projet. Les étapes que vous suivez pour ce faire sont décrites dans Prise d’un projet C++/CX et ajout de la prise en charge de C++/WinRT. Dans cette section, l’étape sur l’ajout du
interop_helpers.hfichier d’en-tête à votre projet est particulièrement importante, car nous allons dépendre de ces fonctions d’assistance dans cette rubrique. - Enfin, ajoutez
#include <pplawait.h>àpch.h. Cela vous donne la prise en charge coroutine pour PPL (il existe plus d’informations sur cette prise en charge dans la section suivante).
Ne compilez pas encore, sinon vous obtiendrez des erreurs indiquant que byte est ambigu. Voici comment résoudre ce problème.
- Ouvrez
BasicLoader.cpp, puis commentezusing namespace std;. - Dans ce même fichier de code source, vous devez ensuite qualifier shared_ptr en tant que std ::shared_ptr. Pour ce faire, vous pouvez effectuer une recherche et remplacer dans ce fichier.
- Ensuite, qualifiez le vecteur comme std ::vector et la chaîne comme std ::string.
Le projet se compile de nouveau, prend en charge C++/WinRT et contient les fonctions utilitaires d’interopérabilité from_cx et to_cx.
Vous disposez maintenant du projet Simple3DGameDX, prêt pour suivre les explications pas à pas du code de cette rubrique.
Vue d’ensemble du portage de C++/CX asynchrone vers C++/WinRT
En bref, lors du portage, nous remplacerons les chaînes de tâches PPL par des appels à co_await. Nous allons modifier la valeur de retour d’une méthode d’une tâche PPL en objet winrt ::IAsyncXxx C++/WinRT. Et nous remplacerons également tout IAsyncXxx^ par un winrt::IAsyncXxx de C++/WinRT.
Vous vous souviendrez qu’une coroutine est toute méthode qui appelle co_xxx. Une coroutine C++/WinRT utilise co_return pour retourner de manière coopérative sa valeur. Grâce à la prise en charge des coroutines pour PPL (grâce à pplawait.h), vous pouvez également utiliser co_return pour renvoyer une tâche PPL depuis une coroutine. Vous pouvez également effectuer co_await des tâches et IAsyncXxx. Mais vous ne pouvez pas utiliser co_return pour retourner un IAsyncXxx^. Le tableau ci-dessous décrit la prise en charge de l’interopérabilité entre les différentes techniques asynchrones illustrées par pplawait.h dans la figure.
| Méthode | Pouvez-vous co_await le faire ? |
Pouvez-vous co_return en partir ? |
|---|---|---|
| La méthode retourne tâche<void> | Yes | Yes |
| La méthode retourne Task<T> | No | Yes |
| La méthode retourne IAsyncXxx^ | Yes | Non. Mais vous encapsulez create_async autour d’une tâche qui utilise co_return. |
| La méthode retourne winrt ::IAsyncXxx | Yes | Yes |
Utilisez ce tableau suivant pour accéder directement à la section de cette rubrique qui décrit une technique d’interopérabilité d’intérêt, ou simplement continuer la lecture à partir d’ici.
| Technique d’interopérabilité asynchrone | Section de cette rubrique |
|---|---|
Utilisez co_await pour attendre une méthode task<void> dans une méthode fire-and-forget, ou dans un constructeur. |
Await task<void> dans une méthode de type fire-and-forget |
Utilisez co_await pour attendre une méthode task<void> depuis une méthode task<void>. |
Utiliser await <task>void dans une méthode task<void> |
Utilisez co_await pour attendre une méthode task<void> depuis une méthode task<T>. |
Attendez task<void> dans une méthode task<T> |
Utilisez co_await pour attendre une méthode IAsyncXxx^. |
Attendez un IAsyncXxx^ dans une méthode de tâche , en laissant le reste du projet inchangé |
Utiliser co_return dans une méthode task<void>. |
Utiliser task<void> au sein d’une méthode task<void> |
Utiliser co_return dans une méthode task<T>. |
Attendez un IAsyncXxx^ dans une méthode de tâche , en laissant le reste du projet inchangé |
Encapsulez create_async autour d’une tâche qui utilise co_return. |
Encapsuler create_async autour d’une tâche qui utilise co_return |
| Concurrence de port ::wait. |
Porter concurrency::wait vers co_await winrt::resume_after |
| Renvoyez winrt::IAsyncXxx au lieu de task<void>. | Porter un type de retour void< de tâche> vers winrt ::IAsyncXxx |
| Convertissez un winrt::IAsyncXxx<T> (T est un type primitif) en une tâche<T>. | Convertir un winrt ::IAsyncXxx<T> (T est primitif) en une tâche<T> |
| Convertissez un winrt::IAsyncXxx<T> (T est un type Windows Runtime) en un task<T^>. | Convertir un winrt ::IAsyncXxx<T> (T est un type Windows Runtime) en une tâche<T^> |
Voici un exemple de code court illustrant une partie de la prise en charge.
#include <ppltasks.h>
#include <pplawait.h>
#include <winrt/Windows.Foundation.h>
concurrency::task<bool> TaskAsync()
{
co_return true;
}
Windows::Foundation::IAsyncOperation<bool>^ IAsyncXxxCppCXAsync()
{
// co_return true; // Error! Can't do that. But you can do
// the following.
return concurrency::create_async([=]() -> concurrency::task<bool> {
co_return true;
});
}
winrt::Windows::Foundation::IAsyncOperation<bool> IAsyncXxxCppWinRTAsync()
{
co_return true;
}
concurrency::task<bool> CppCXAsync()
{
bool b1 = co_await TaskAsync();
bool b2 = co_await IAsyncXxxCppCXAsync();
co_return co_await IAsyncXxxCppWinRTAsync();
}
winrt::fire_and_forget CppWinRTAsync()
{
bool b1 = co_await TaskAsync();
bool b2 = co_await IAsyncXxxCppCXAsync();
bool b3 = co_await IAsyncXxxCppWinRTAsync();
}
Important
Même avec ces grandes options d’interopérabilité, le portage progressif dépend du choix des modifications que nous pouvons apporter chirurgicalement qui n’affectent pas le reste du projet. Nous voulons éviter de tirer sur un fil qui dépasse pris au hasard et de défaire ainsi toute la structure du projet. Pour cela, nous devons faire des choses dans un ordre particulier. Nous allons ensuite examiner de près quelques exemples de la manière d’effectuer ce type de modifications de portage/d’interopérabilité liées à l’asynchrone.
Attendez une méthode task<void>, sans modifier le reste du projet
Une méthode qui renvoie task<void> exécute un travail de manière asynchrone et renvoie un objet d’opération asynchrone, mais elle ne produit pas de valeur au final. On peut co_await utiliser une méthode de ce type.
Ainsi, un bon endroit pour commencer à porter du code asynchrone progressivement consiste à trouver des emplacements où vous appelez ces méthodes. Ces emplacements nécessiteront la création et/ou le renvoi d’une tâche. Ils peuvent également impliquer le type de chaîne de tâches où aucune valeur n’est passée de chaque tâche à sa continuation. Dans ce genre de cas, vous pouvez simplement remplacer le code asynchrone par des instructions co_await, comme nous allons le voir.
Note
À mesure que cette rubrique progresse, vous verrez l’avantage de cette stratégie. Une fois qu’une méthode task void< particulière> est appelée exclusivement via co_await, vous êtes alors libre de porter cette méthode vers C++/WinRT et de renvoyer un winrt ::IAsyncXxx.
Trouvons quelques exemples. Ouvrez le projet Simple3DGameDX (voir l’exemple de jeu Direct3D).
Important
Dans les exemples qui suivent, comme vous voyez les implémentations des méthodes modifiées, n’oubliez pas que nous n’avons pas besoin de modifier les appelants des méthodes que nous modifions. Ces modifications sont localisées et ne sont pas en cascade dans le projet.
Attendre task<void> dans une méthode de type « fire-and-forget »
Commençons par attendre task<void> dans les méthodes fire-and-forget, car c’est le cas le plus simple. Il s’agit de méthodes qui fonctionnent de manière asynchrone, mais l’appelant de la méthode n’attend pas que ce travail se termine. Il vous suffit d’appeler la méthode et de ne plus vous en soucier, même si elle s’exécute de manière asynchrone.
Examinez la racine du graphique de dépendances de votre projet pour les void méthodes qui contiennent create_task et/ou des chaînes de tâches où seules les méthodes task<void> sont appelées.
Dans Simple3DGameDX, vous trouverez du code comme celui dans l’implémentation de la méthode GameMain ::Update. Il se trouve dans le fichier GameMain.cppde code source .
GameMain ::Update
Voici un extrait de la version C++/CX de la méthode, montrant les deux parties de la méthode qui se terminent de façon asynchrone.
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());
...
...
}
Vous pouvez voir un appel à la méthode Simple3DGame::LoadLevelAsync (qui renvoie un task<void> PPL). Ensuite vient une continuation qui exécute un traitement synchrone. LoadLevelAsync est asynchrone, mais ne retourne pas de valeur. Par conséquent, aucune valeur n’est passée de la tâche à la continuation.
Nous pouvons apporter le même type de modification au code dans ces deux emplacements. Le code est expliqué après la liste ci-dessous. Nous pourrions discuter ici de la manière sûre d’accéder au pointeur this dans une coroutine membre d’une classe. Mais nous allons différer cela pour une section ultérieure (la discussion différée sur co_await et le pointeur)— pour l’instant, ce code fonctionne.
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;
...
...
}
Comme vous pouvez le voir, étant donné que LoadLevelAsync retourne une tâche, nous pouvons co_await le faire. Et nous n’avons pas besoin d’une continuation explicite : le code qui suit une co_await exécution uniquement lorsque LoadLevelAsync est terminé.
L’ajout de co_await transforme la méthode en coroutine, nous ne pouvions donc pas la laisser renvoyer void. C’est une méthode fire-and-forget, donc nous l’avons changée pour retourner winrt ::fire_and_forget.
Vous devrez également modifier GameMain.h. Remplacez également le type de retour de GameMain::Update de void par winrt::fire_and_forget dans cette déclaration.
Vous pouvez apporter cette modification à votre copie du projet, et le jeu se compile et s’exécute toujours de la même manière. Le code source est toujours fondamentalement C++/CX, mais il utilise désormais les mêmes modèles que C++/WinRT, ce qui nous a déplacé un peu plus près pour pouvoir porter le reste du code mécaniquement.
GameMain ::ResetGame
GameMain::ResetGame est une autre méthode de type fire-and-forget ; elle appelle aussi LoadLevelAsync. Vous pouvez donc modifier le même code si vous le souhaitez.
GameMain ::OnDeviceRestored
Les choses sont un peu plus intéressantes dans GameMain ::OnDeviceRestored en raison de son imbrication plus approfondie du code asynchrone, y compris une tâche no-op. Voici un aperçu des parties asynchrones de cette méthode (le code synchrone, moins intéressant, étant représenté par des points de suspension).
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());
}
Tout d’abord, modifiez le type de retour de GameMain::OnDeviceRestored de void en winrt::fire_and_forget dans GameMain.h et .cpp. Vous devez également ouvrir DeviceResources.h et apporter la même modification au type de retour IDeviceNotify ::OnDeviceRestored.
Pour porter le code asynchrone, supprimez tous les appels à create_task et à then, ainsi que leurs accolades, puis simplifiez la méthode en une simple suite linéaire d’instructions.
Modifiez tout return qui renvoie une tâche en co_await. Vous serez laissé avec un return qui ne retourne rien, donc simplement supprimer cela. Lorsque vous avez terminé, la tâche no-op a disparu, et le plan des parties asynchrones de la méthode se présente comme suit. Là encore, le code synchrone moins intéressant est supprimé.
winrt::fire_and_forget GameMain::OnDeviceRestored()
{
...
co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
...
if (m_updateState == UpdateEngineState::WaitingForResources)
{
co_await m_game->LoadLevelAsync();
...
}
...
}
Comme vous pouvez le voir, cette forme de structure asynchrone est considérablement plus simple et plus facile à lire.
GameMain ::GameMain
Le constructeur GameMain ::GameMain effectue un travail asynchrone et aucune partie du projet n’attend la fin de ce travail. Là encore, cette liste décrit les parties asynchrones.
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());
}
Mais un constructeur ne peut pas retourner winrt ::fire_and_forget. Nous allons donc déplacer le code asynchrone dans une nouvelle méthode GameMain ::ConstructInBackground fire-and-forget, aplatir le code en co_await instructions et appeler la nouvelle méthode du constructeur. Voici le résultat.
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();
...
}
...
}
Désormais, toutes les méthodes de type « fire-and-forget » — en fait, tout le code asynchrone — dans GameMain ont été transformés en coroutines. Si le cœur vous en dit, vous pourriez peut-être chercher des méthodes sans attente de résultat dans d’autres classes et effectuer des modifications similaires.
La discussion différée sur co_await et le pointeur this
Lorsque nous avons apporté des modifications à GameMain ::Update, j’ai différé une discussion sur ce pointeur. Discutons-en ici.
Cela s’applique à toutes les méthodes que nous avons modifiées jusqu’à présent ; et cela s’applique à toutes les coroutines, pas seulement à celles lancées sans attendre leur résultat. L’introduction d’un co_await dans une méthode introduit un point de suspension de l’exécution. Et à cause de cela, nous devons faire attention au pointeur this, que nous utilisons bien sûr après le point de suspension chaque fois que nous accédons à un membre de la classe.
La courte histoire est que la solution consiste à appeler implémente ::get_strong. Toutefois, pour une discussion complète du problème et de la solution, consultez Accéder en toute sécurité au pointeur this dans une coroutine de membre de classe.
Vous pouvez appeler implements ::get_strong uniquement dans une classe qui dérive de winrt ::implements.
Dériver GameMain de winrt ::implements
Le premier changement que nous devons apporter est en GameMain.h.
class GameMain :
public DX::IDeviceNotify
GameMain continuera d’implémenter DX ::IDeviceNotify, mais nous allons le modifier pour dériver de winrt ::implements.
class GameMain :
public winrt::implements<GameMain, winrt::Windows::Foundation::IInspectable>,
DX::IDeviceNotify
Ensuite, dans App.cpp, vous trouverez cette méthode.
void App::Load(Platform::String^)
{
if (!m_main)
{
m_main = std::unique_ptr<GameMain>(new GameMain(m_deviceResources));
}
}
Mais maintenant que GameMain dérive de winrt ::implements, nous devons le construire d’une manière différente. Dans ce cas, nous allons utiliser le modèle de fonction winrt ::make_self . Pour plus d’informations, consultez Instanciation et retour des types et interfaces d’implémentation.
Remplacez cette ligne de code par celle-ci.
...
m_main = winrt::make_self<GameMain>(m_deviceResources);
...
Pour fermer la boucle sur cette modification, nous devons également modifier le type de m_main. Dans App.h, vous trouverez ce code.
ref class App sealed :
public Windows::ApplicationModel::Core::IFrameworkView
{
...
private:
...
std::unique_ptr<GameMain> m_main;
};
Modifiez cette déclaration de m_main à ceci.
...
winrt::com_ptr<GameMain> m_main;
...
Nous pouvons maintenant appeler implements::get_strong
Pour GameMain ::Update et pour l’une des autres méthodes auxquelles nous avons ajouté un co_await , voici comment vous pouvez appeler get_strong au début d’une coroutine pour vous assurer qu’une référence forte survive jusqu’à ce que la coroutine se termine.
winrt::fire_and_forget GameMain::Update()
{
auto strong_this{ get_strong() }; // Keep *this* alive.
...
co_await ...
...
}
Attendre task<void> au sein d’une méthode task<void>
Le cas simple suivant consiste à attendre task<void> dans une méthode qui renvoie elle-même task<void>. C’est parce que nous pouvons co_await une tâche<void>, et nous pouvons co_return à partir d’une telle tâche.
Vous trouverez un exemple très simple dans l’implémentation de la méthode Simple3DGame ::LoadLevelAsync. Il se trouve dans le fichier Simple3DGame.cppde code source .
task<void> Simple3DGame::LoadLevelAsync()
{
m_level[m_currentLevel]->Initialize(m_objects);
m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
return m_renderer->LoadLevelResourcesAsync();
}
Il existe simplement du code synchrone, suivi du renvoi de la tâche créée par GameRenderer ::LoadLevelResourcesAsync.
Au lieu de retourner cette tâche, nous la co_await, puis co_return le void résultant.
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();
}
Cela ne ressemble pas à un changement profond. Mais maintenant que nous appelons GameRenderer::LoadLevelResourcesAsync via co_await, nous pouvons l’adapter pour qu’elle renvoie un winrt::IAsyncXxx au lieu d’une tâche. Nous allons le faire plus loin dans la section Port a task<void> return type to winrt ::IAsyncXxx.
Utiliser await dans une méthode task<T> avec task<void>
Bien qu’il n’y ait pas d’exemples appropriés à trouver dans Simple3DGameDX, nous pouvons contrive un exemple hypothétique simplement pour afficher le modèle.
La première ligne de l’exemple de code ci-dessous montre le simple co_await de task<void>. Ensuite, pour satisfaire le type de retour T< de la tâche>, nous devons retourner de façon asynchrone un StorageFile^. Pour ce faire, nous avons co_await une API Windows Runtime et co_return le fichier résultant.
task<StorageFile^> Simple3DGame::LoadLevelAndRetrieveFileAsync(
StorageFolder^ location,
Platform::String^ filename)
{
co_await m_renderer->LoadLevelResourcesAsync();
co_return co_await location->GetFileAsync(filename);
}
Nous pourrions même porter davantage de méthode vers C++/WinRT comme ceci.
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);
}
Le membre de données m_renderer est toujours en C++/CX dans cet exemple.
Attendez un IAsyncXxx^ dans une méthode de tâche , en laissant le reste du projet inchangé
Nous avons vu comment utiliser co_awaittask<void>. Vous pouvez également co_await une méthode qui retourne un IAsyncXxx, qu'il s'agisse d'une méthode dans votre projet ou d'une API de Windows asynchrone (par exemple, StorageFolder.GetFileAsync, que nous avons attendue de manière coopérative dans la section précédente).
Pour obtenir un exemple de modification de ce type de code, examinons BasicReaderWriter ::ReadDataAsync (vous le trouverez implémenté dans BasicReaderWriter.cpp).
Voici la version C++/CX d’origine.
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;
});
}
La liste de codes ci-dessous montre que nous pouvons co_await Windows API qui retournent IAsyncXxx^. Non seulement cela, nous pouvons également co_return la valeur que BasicReaderWriter ::ReadDataAsync retourne de façon asynchrone (dans ce cas, un tableau d’octets). Cette première étape montre comment apporter uniquement ces modifications ; Nous allons en fait porter le code C++/CX vers C++/WinRT dans la section suivante.
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;
}
Là encore, nous n’avons pas besoin de modifier les appelants des méthodes que nous changeons, car nous n’avons pas modifié le type de retour.
Port ReadDataAsync (principalement) vers C++/WinRT, laissant le reste du projet inchangé
Nous pouvons aller plus loin et porter la méthode presque entièrement vers C++/WinRT sans avoir à modifier une autre partie du projet.
La seule dépendance de cette méthode vis-à-vis du reste du projet est le membre de données BasicReaderWriter::m_location, qui est un StorageFolder^ C++/CX. Pour laisser ce membre de données inchangé et laisser le type de paramètre et le type de retour inchangés, nous n’avons besoin d’effectuer que quelques conversions, une au début de la méthode et une à la fin. Pour cela, nous pouvons utiliser les fonctions d’assistance d’interopérabilité from_cx et to_cx.
Voici comment BasicReaderWriter ::ReadDataAsync s’occupe du portage de son implémentation principalement vers C++/WinRT. Il s’agit d’un bon exemple de portage progressif. Et cette méthode est à l’étape où nous pouvons nous éloigner de la pensée comme une méthode C++/CX qui utilise certaines techniques C++/WinRT, et la voir comme une méthode C++/WinRT qui interopére avec 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
Dans ReadDataAsync ci-dessus, nous construisons et renvoyons un nouveau tableau C++/CX. Et bien sûr, nous faisons cela pour satisfaire le type de retour de la méthode (de sorte que nous n’avons pas à modifier le reste du projet).
Vous pouvez rencontrer d’autres exemples dans votre propre projet où, après le portage, vous atteignez la fin de la méthode et tout ce que vous avez est un objet C++/WinRT. Pour co_return, appelez simplement to_cx pour effectuer la conversion. Il y a plus d’informations sur cela, et un exemple, la section suivante.
Convertir un winrt::IAsyncXxx<T> en task<T>
Cette section traite de la situation où vous avez porté une méthode asynchrone vers C++/WinRT (afin qu’elle retourne un winrt ::IAsyncXxx<T>), mais que vous avez toujours du code C++/CX appelant cette méthode comme s’il retourne toujours une tâche.
- L’un des cas est celui où T est primitif, qui n’a pas besoin de conversion.
- L'autre cas est celui où T est un type Windows Runtime, auquel cas vous devez le convertir en T^.
Convertir un winrt ::IAsyncXxx<T> (T est primitif) en une tâche<T>
Le modèle de cette section s’applique lorsque vous retournez de façon asynchrone une valeur primitive (nous allons utiliser une valeur booléenne pour illustrer). Prenons un exemple où une méthode que vous avez déjà transférée vers C++/WinRT a cette signature.
winrt::Windows::Foundation::IAsyncOperation<bool>
MyClass::GetBoolMemberFunctionAsync()
{
bool value = ...
co_return value;
}
Vous pouvez convertir un appel en cette méthode en une tâche comme celle-ci.
task<bool> MyClass::RetrieveBoolTask()
{
co_return co_await GetBoolMemberFunctionAsync();
}
Ou comme ça.
task<bool> MyClass::RetrieveBoolTask()
{
return concurrency::create_task(
[this]() -> concurrency::task<bool> {
auto result = co_await GetBoolMemberFunctionAsync();
co_return result;
});
}
Notez que le type de retour de tâche de la fonction lambda est explicite, car le compilateur ne peut pas le déduire.
Nous pourrions également appeler la méthode à partir d’une chaîne de tâches arbitraire comme celle-ci. Là encore, avec un type de retour lambda explicite.
...
.then([this]() -> concurrency::task<bool> {
co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
...
});
...
Convertir un winrt ::IAsyncXxx<T> (T est un type Windows Runtime) en une tâche<T^>
Le modèle de cette section s'applique lorsque vous retournez de façon asynchrone une valeur Windows Runtime (nous allons utiliser une valeur StorageFile pour illustrer). Prenons un exemple où une méthode que vous avez déjà transférée vers C++/WinRT a cette signature.
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
MyClass::GetStorageFileMemberFunctionAsync()
{
co_return co_await winrt::Windows::Storage::StorageFile::GetFileFromPathAsync
(L"MyFile.txt");
}
Cette liste suivante montre comment convertir un appel en cette méthode en une tâche. Notez que nous devons appeler la fonction d’assistance d’interopérabilité to_cx pour convertir l’objet C++/WinRT retourné en handle C++/CX (également appelé objet hat).
task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
winrt::Windows::Storage::StorageFile storageFile =
co_await GetStorageFileMemberFunctionAsync();
co_return to_cx<Windows::Storage::StorageFile>(storageFile);
}
Voici une version plus succincte de cela.
task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
co_return to_cx<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}
Et vous pouvez même choisir d’encapsuler ce schéma dans un modèle de fonction réutilisable, et return comme vous retourneriez normalement une tâche.
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());
}
Si vous aimez cette idée, vous pouvez ajouter to_task à interop_helpers.h.
Encapsulez create_async autour d’une tâche utilisant co_return
Vous ne pouvez pas co_return directement un IAsyncXxx^, mais vous pouvez faire quelque chose de similaire. Si vous avez une tâche qui retourne de manière coopérative une valeur, vous pouvez l’encapsuler à l’intérieur d’un appel à concurrency ::create_async.
Voici un exemple hypothétique, car il n’existe pas d’exemple que nous pouvons lever à partir de Simple3DGameDX.
Windows::Foundation::IAsyncOperation<bool>^ MyClass::RetrieveBoolAsync()
{
return concurrency::create_async(
[this]() -> concurrency::task<bool> {
bool result = co_await GetBoolMemberFunctionAsync();
co_return result;
});
}
Comme vous pouvez le voir, vous pouvez obtenir la valeur de retour à partir de n’importe quelle méthode que vous pouvez co_await.
Porter concurrency::wait vers co_await winrt::resume_after
Il existe quelques endroits où Simple3DGameDX utilise concurrency ::wait pour suspendre le thread pendant une courte période. Voici un exemple.
// 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);
}));
...
}
La version C++/WinRT de concurrency::wait est la structure winrt::resume_after. Nous pouvons co_await cette structure dans une tâche PPL. Voici un exemple de code.
// 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);
}));
...
}
Notez les deux autres changements que nous avons dû apporter. Nous avons changé le type de GameConstants ::InitialLoadingDelay en std ::chrono ::d uration, et nous avons rendu le type de retour de la fonction lambda explicite, car le compilateur n’est plus en mesure de le déduire.
Convertir un type de retour task<void> en winrt::IAsyncXxx
Simple3DGame ::LoadLevelAsync
À ce stade de notre travail avec Simple3DGameDX, tous les endroits du projet qui appellent Simple3DGame::LoadLevelAsync utilisent co_await pour effectuer cet appel.
Cela signifie que nous pouvons simplement modifier le type de retour de cette méthode de task<void> à winrt ::Windows ::Foundation ::IAsyncAction (en laissant le reste inchangé).
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();
}
Le portage du reste de cette méthode et de ses dépendances (comme m_level, et ainsi de suite) en C++/WinRT devrait maintenant être assez simple.
GameRenderer ::LoadLevelResourcesAsync
Voici la version C++/CX d’origine de 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 est le seul emplacement dans le projet qui appelle GameRenderer ::LoadLevelResourcesAsync, et il utilise co_await déjà pour l’appeler.
Il n'est donc plus nécessaire pour GameRenderer ::LoadLevelResourcesAsync de retourner une tâche. Il peut donc retourner une tâche winrt ::Windows ::Foundation ::IAsyncAction à la place. Et l’implémentation elle-même est assez simple pour porter complètement vers C++/WinRT. Cela implique d’apporter la même modification que celle que nous avons apportée de Port concurrency::wait à co_await winrt::resume_after. Et il n’y a pas de dépendances significatives sur le reste du projet à s’inquiéter.
Voici à quoi ressemble la méthode une fois entièrement portée vers 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);
}
L’objectif : porter entièrement une méthode vers C++/WinRT
Examinons cette procédure pas à pas avec un exemple de l’objectif final, en portant entièrement la méthode BasicReaderWriter ::ReadDataAsync vers C++/WinRT.
La dernière fois que nous avons examiné cette méthode (dans la section Port ReadDataAsync (principalement) vers C++/WinRT, laissant le reste du projet inchangé), elle a été principalement porté vers C++/WinRT. Mais il a toujours renvoyé une tâche de type 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());
}
Au lieu de retourner une tâche, nous allons la modifier pour renvoyer un IAsyncOperation. Et au lieu de retourner un tableau d’octets via cet IAsyncOperation, nous retournerons plutôt un objet IBuffer C++/WinRT. Cela nécessite également une modification mineure du code sur les sites d’appel, comme nous le verrons.
Voici à quoi ressemble la méthode après le portage de son implémentation, de son paramètre et du membre de données m_location afin d’utiliser la syntaxe et les objets 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() };
}
Comme vous pouvez le voir, BasicReaderWriter ::ReadDataAsync lui-même est beaucoup plus simple, car nous avons factorisé dans sa propre méthode la logique synchrone qui récupère les octets de la mémoire tampon.
Mais maintenant, nous devons porter les sites d’appel à partir de ce type de structure en C++/CX.
task<void> BasicLoader::LoadTextureAsync(...)
{
return m_basicReaderWriter->ReadDataAsync(filename).then(
[=](const Platform::Array<byte>^ textureData)
{
CreateTexture(...);
});
}
À ce modèle en C++/WinRT.
winrt::Windows::Foundation::IAsyncAction BasicLoader::LoadTextureAsync(...)
{
auto textureBuffer = co_await m_basicReaderWriter.ReadDataAsync(filename);
auto textureData = GetBufferView(textureBuffer);
CreateTexture(...);
}
API importantes
- IAsyncAction
- IAsyncActionWithProgress<TProgress>
- IAsyncOperation<TResult>
- IAsyncOperationWithProgress<TResult, TProgress>
- implements ::get_strong
- concurrency::create_async
- concurrency::create_task
- concurrency::task
- concurrency::task::then
- concurrency ::wait
- winrt ::fire_and_forget
- winrt ::make_self
Rubriques connexes
Windows developer