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.
Cette rubrique décrit les scénarios avancés de concurrence et d’asynchronisme en C++/WinRT.
Pour une introduction à ce sujet, commencez par lire les opérations simultanées et asynchrones.
Confier le travail au pool de threads Windows
Une coroutine est une fonction comme n’importe quelle autre, en ce sens qu’un appelant est bloqué jusqu’à ce qu’une fonction lui rende l’exécution. Et, la première occasion pour un coroutine de retourner est la première co_await, co_returnou co_yield.
Par conséquent, avant d’effectuer un travail lié au calcul dans une coroutine, vous devez retourner l’exécution à l’appelant (en d’autres termes, introduire un point de suspension) afin que l’appelant ne soit pas bloqué. Si vous ne faites pas déjà cela en co_awaiteffectuant une autre opération, vous pouvez co_await la fonction winrt ::resume_background . Cela rend le contrôle à l’appelant, puis reprend immédiatement l’exécution sur un thread du pool de threads.
Le pool de threads utilisé dans l'implémentation est le pool de threads Windows de bas niveau, il offre donc une efficacité optimale.
IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
co_await winrt::resume_background(); // Return control; resume on thread pool.
uint32_t result;
for (uint32_t y = 0; y < height; ++y)
for (uint32_t x = 0; x < width; ++x)
{
// Do compute-bound work here.
}
co_return result;
}
Programmer en tenant compte de l’affinité des threads
Ce scénario s’étend sur le précédent. Vous confiez une partie du travail au pool de threads, mais vous souhaitez ensuite afficher l’avancement dans l’interface utilisateur.
IAsyncAction DoWorkAsync(TextBlock textblock)
{
co_await winrt::resume_background();
// Do compute-bound work here.
textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}
Le code ci-dessus lève une exception winrt ::hresult_wrong_thread , car un TextBlock doit être mis à jour à partir du thread qui l’a créé, qui est le thread d’interface utilisateur. Une solution consiste à capturer le contexte du thread dans lequel notre coroutine a été appelée initialement. Pour ce faire, instanciez un objet winrt ::apartment_context , effectuez un travail en arrière-plan, puis co_await le apartment_context pour revenir au contexte appelant.
IAsyncAction DoWorkAsync(TextBlock textblock)
{
winrt::apartment_context ui_thread; // Capture calling context.
co_await winrt::resume_background();
// Do compute-bound work here.
co_await ui_thread; // Switch back to calling context.
textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}
Tant que la coroutine ci-dessus est appelée à partir du thread d’interface utilisateur qui a créé TextBlock, cette technique fonctionne. Il y aura de nombreux cas dans votre application où vous êtes certain de cela.
Pour une solution plus générale pour mettre à jour l’interface utilisateur, qui couvre les cas où vous ne savez pas avec certitude quel est le thread appelant, vous pouvez co_await utiliser la fonction winrt::resume_foreground pour basculer vers un thread de premier plan donné. Dans l’exemple de code ci-dessous, nous spécifions le thread de premier plan en passant la file d’attente du répartiteur associée au TextBlock (en accédant à sa propriété DispatcherQueue ). L’implémentation de winrt::resume_foreground appelle DispatcherQueue.TryEnqueue sur cet objet DispatcherQueue pour exécuter le travail qui la suit dans la coroutine.
IAsyncAction DoWorkAsync(TextBlock textblock)
{
co_await winrt::resume_background();
// Do compute-bound work here.
// Switch to the foreground thread associated with textblock.
co_await winrt::resume_foreground(textblock.DispatcherQueue());
textblock.Text(L"Done!"); // Guaranteed to work.
}
La fonction winrt ::resume_foreground prend un paramètre de priorité facultatif. Si vous utilisez ce paramètre, le modèle indiqué ci-dessus est approprié. Si ce n’est pas le cas, vous pouvez choisir de simplifier co_await winrt::resume_foreground(someDispatcherObject); en seulement co_await someDispatcherObject;.
Contextes d’exécution, reprise et changement de contexte au sein d’une coroutine
En général, après un point de suspension dans une coroutine, le thread d’origine de l’exécution peut disparaître et la reprise peut se produire sur n’importe quel thread (en d’autres termes, tout thread peut appeler la méthode Completed pour l’opération asynchrone).
Mais si vous co_await l’un des quatre types d’opération asynchrone de Windows Runtime (IAsyncXxx), C++/WinRT capture le contexte de l’appelant au moment où vous co_await. Et cela garantit que vous êtes toujours dans ce contexte lorsque la continuation reprend. C++/WinRT effectue cette opération en vérifiant si vous êtes déjà sur le contexte d’appel et, si ce n’est pas le cas, en y basculant. Si vous étiez sur un thread d’appartement à thread unique (STA) avant co_await, vous serez sur le même par la suite ; si vous étiez sur un thread d’appartement multithread (MTA) avant co_await, vous en serez sur un autre par la suite.
IAsyncAction ProcessFeedAsync()
{
Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
SyndicationClient syndicationClient;
// The thread context at this point is captured...
SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
// ...and is restored at this point.
}
La raison pour laquelle vous pouvez vous appuyer sur ce comportement est que C++/WinRT fournit du code permettant d’adapter ces types d’opérations asynchrones du Windows Runtime au support du langage des coroutines en C++ (ces fragments de code sont appelés adaptateurs d’attente). Les autres types pouvant être attendus dans C++/WinRT sont simplement des adaptateurs de pool de threads et/ou des utilitaires ; ils s’achèvent donc sur le pool de threads.
using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
// No matter what the thread context is at this point...
co_await 5s;
// ...we're on the thread pool at this point.
co_return 123;
}
Si vous co_await avez un autre type, même dans une implémentation coroutine C++/WinRT, une autre bibliothèque fournit les adaptateurs et vous devez comprendre ce que font ces adaptateurs en termes de reprise et de contextes.
Pour réduire au minimum les changements de contexte, vous pouvez utiliser certaines des techniques que nous avons déjà vues dans cette rubrique. Voyons quelques illustrations de cette opération. Dans cet exemple de pseudo-code suivant, nous affichons le contour d’un gestionnaire d’événements qui appelle une API Windows Runtime pour charger une image, tombe sur un thread d’arrière-plan pour traiter cette image, puis retourne au thread d’interface utilisateur pour afficher l’image dans l’interface utilisateur.
IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
// We begin in the UI context.
// Call StorageFile::OpenAsync to load an image file.
// The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.
co_await winrt::resume_background();
// We're now on a background thread.
// Process the image.
co_await winrt::resume_foreground(this->DispatcherQueue());
// We're back on MainPage's UI thread.
// Display the image in the UI.
}
Pour ce scénario, il existe un peu d’inefficacité autour de l’appel à StorageFile ::OpenAsync. Un changement de contexte vers un thread en arrière-plan est nécessaire (afin que le gestionnaire puisse rendre la main à l’appelant), puis, lors de la reprise, C++/WinRT restaure le contexte du thread d’interface utilisateur. Toutefois, dans ce cas, il n’est pas nécessaire d’être sur le thread d’interface utilisateur tant que nous n’avons pas à mettre à jour l’interface utilisateur. Plus nous appelons d’API Windows Runtime avant notre appel à winrt::resume_background, plus nous subissons de changements de contexte inutiles dans un sens puis dans l’autre. La solution consiste à n’appeler aucune API Windows Runtime avant cela. Déplacez-les toutes après le winrt ::resume_background.
IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
// We begin in the UI context.
co_await winrt::resume_background();
// We're now on a background thread.
// Call StorageFile::OpenAsync to load an image file.
// Process the image.
co_await winrt::resume_foreground(this->DispatcherQueue());
// We're back on MainPage's UI thread.
// Display the image in the UI.
}
Si vous souhaitez faire quelque chose de plus avancé, vous pourriez écrire vos propres adaptateurs pour await. Par exemple, si vous souhaitez co_await reprendre sur le même thread que celui sur lequel l’action asynchrone se termine (par conséquent, il n’y a pas de commutateur de contexte), vous pouvez commencer par écrire des adaptateurs await similaires à ceux indiqués ci-dessous.
Note
L’exemple de code ci-dessous est fourni uniquement à des fins éducatives ; c’est pour vous aider à comprendre comment fonctionnent les adaptateurs Await. Si vous souhaitez utiliser cette technique dans votre propre base de code, nous vous recommandons de développer et de tester votre ou vos propres structure(s) d’adaptation pour await. Par exemple, vous pouvez écrire complete_on_any, complete_on_current et complete_on(dispatcher). Envisagez également de les rendre modèles qui prennent le type IAsyncXxx comme paramètre de modèle.
struct no_switch
{
no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
{
}
bool await_ready() const
{
return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
}
void await_suspend(std::experimental::coroutine_handle<> handle) const
{
m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
{
handle();
});
}
auto await_resume() const
{
return m_async.GetResults();
}
private:
Windows::Foundation::IAsyncAction const& m_async;
};
Pour comprendre comment utiliser les adaptateurs no_switch await, vous devez d’abord savoir que lorsque le compilateur C++ rencontre une co_await expression, elle recherche des fonctions appelées await_ready, await_suspend et await_resume. La bibliothèque C++/WinRT fournit ces fonctions afin que vous obteniez un comportement raisonnable par défaut, comme ceci.
IAsyncAction async{ ProcessFeedAsync() };
co_await async;
Pour utiliser les adaptateurs no_switch await, remplacez simplement le type de cette co_await expression par IAsyncXxx par no_switch, comme suit.
IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);
Ensuite, au lieu de rechercher les trois fonctions await_xxx qui correspondent à IAsyncXxx, le compilateur C++ recherche les fonctions qui correspondent no_switch.
Une plongée plus approfondie dans winrt ::resume_foreground
Depuis C++/WinRT 2.0, la fonction winrt ::resume_foreground s’interrompt même si elle est appelée à partir du thread de répartiteur (dans les versions précédentes, elle peut introduire des interblocages dans certains scénarios, car elle n’est suspendue que si elle n’est pas déjà sur le thread de répartiteur).
Le comportement actuel signifie que vous pouvez compter sur le fait que le déroulement de la pile et la remise en file d’attente ont bien lieu, ce qui est important pour la stabilité du système, en particulier dans le code système de bas niveau. Le dernier extrait de code de la section Programmation en tenant compte de l’affinité des threads, ci-dessus, illustre l’exécution d’un calcul complexe sur un thread en arrière-plan, avant de basculer vers le thread d’interface utilisateur approprié afin de mettre à jour l’interface utilisateur (IU).
Voici comment winrt ::resume_foreground ressemble en interne.
auto resume_foreground(...) noexcept
{
struct awaitable
{
bool await_ready() const
{
return false; // Queue without waiting.
// return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
}
void await_resume() const {}
void await_suspend(coroutine_handle<> handle) const { ... }
};
return awaitable{ ... };
};
Ce comportement actuel, par rapport au précédent, est analogue à la différence entre PostMessage et SendMessage dans le développement d’applications Win32. PostMessage met en file d’attente le travail, puis décompresse la pile sans attendre la fin du travail. Le déroulage de pile peut être essentiel.
La fonction winrt ::resume_foreground a initialement pris en charge le CoreDispatcher (lié à un CoreWindow), qui a été introduit avant Windows 10. Dans Les applications WinUI 3 et SDK d'application Windows, utilisez plutôt dispatcherQueue. Vous pouvez créer un DispatcherQueue à vos propres fins. Considérez cette application console simple.
using namespace Windows::System;
winrt::fire_and_forget RunAsync(DispatcherQueue queue);
int main()
{
auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
RunAsync(controller.DispatcherQueue());
getchar();
}
L’exemple ci-dessus crée une file d’attente (contenue dans un contrôleur) sur un thread privé, puis transmet le contrôleur à la coroutine. La coroutine peut utiliser la file d’attente pour attendre (suspendre et reprendre) sur le thread privé. Une autre utilisation courante de DispatcherQueue consiste à créer une file d’attente sur le thread d’interface utilisateur actuel pour une application de bureau ou Win32 traditionnelle.
DispatcherQueueController CreateDispatcherQueueController()
{
DispatcherQueueOptions options
{
sizeof(DispatcherQueueOptions),
DQTYPE_THREAD_CURRENT,
DQTAT_COM_STA
};
ABI::Windows::System::IDispatcherQueueController* ptr{};
winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
return { ptr, take_ownership_from_abi };
}
Cela montre comment appeler et incorporer des fonctions Win32 dans vos projets C++/WinRT, en appelant simplement la fonction CreateDispatcherQueueController de style Win32 pour créer le contrôleur, puis transférer la propriété du contrôleur de file d’attente obtenu à l’appelant en tant qu’objet WinRT. C’est également précisément la façon dont vous pouvez prendre en charge une mise en file d’attente efficace et transparente sur votre application de bureau Win32 de style Petzold existante.
winrt::fire_and_forget RunAsync(DispatcherQueue queue);
int main()
{
Window window;
auto controller{ CreateDispatcherQueueController() };
RunAsync(controller.DispatcherQueue());
MSG message;
while (GetMessage(&message, nullptr, 0, 0))
{
DispatchMessage(&message);
}
}
Ci-dessus, la fonction principale simple commence par créer une fenêtre. Vous pouvez imaginer que cela inscrit une classe de fenêtre et appelle CreateWindow pour créer la fenêtre de bureau de niveau supérieur. CreateDispatcherQueueController est ensuite appelée pour créer le contrôleur de file d’attente avant d’appeler une coroutine avec la file d’attente du répartiteur gérée par ce contrôleur. Une pompe de message traditionnelle est ensuite entrée où la reprise de la coroutine se produit naturellement sur ce thread. Après cela, vous pouvez revenir au monde élégant des coroutines pour votre flux de travail asynchrone ou basé sur des messages au sein de votre application.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
... // Begin on the calling thread...
co_await winrt::resume_foreground(queue);
... // ...resume on the dispatcher thread.
}
L’appel à winrt ::resume_foreground est toujours en file d’attente, puis décompresse la pile. Vous pouvez également définir la priorité de reprise.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
...
}
Vous pouvez également utiliser l’ordre de mise en file d’attente par défaut.
...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await queue;
...
}
Note
Comme indiqué ci-dessus, veillez à inclure l’en-tête de projection de l’espace de noms du type que vous utilisez co_await. Par exemple, Windows::System::DispatcherQueue ou Microsoft::UI::Dispatching::DispatcherQueue.
Ou, dans ce cas, détecter l’arrêt de la file d’attente et le gérer correctement.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
if (co_await queue)
{
... // Resume on dispatcher thread.
}
else
{
... // Still on calling thread.
}
}
L’expression co_await retourne true, indiquant que la reprise aura lieu sur le thread du répartiteur. En d’autres termes, la mise en attente a été effectuée avec succès. À l’inverse, elle retourne false pour indiquer que l’exécution reste sur le thread appelant, car le contrôleur de la file d’attente s’arrête et ne sert plus les demandes de file d’attente.
Ainsi, vous disposez d’une grande puissance à portée de main en combinant C++/WinRT avec des coroutines, en particulier lorsque vous développez des applications de bureau dans le style Petzold à l’ancienne.
Annulation d’une opération asynchrone et rappels d’annulation
Les fonctionnalités de Windows Runtime pour la programmation asynchrone vous permettent d'annuler une action ou une opération asynchrone en cours d'exécution. Voici un exemple qui appelle StorageFolder ::GetFilesAsync pour récupérer une collection potentiellement volumineuse de fichiers et stocke l’objet d’opération asynchrone résultant dans un membre de données. L’utilisateur a la possibilité d’annuler l’opération.
// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...
// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
MainPage()
{
InitializeComponent();
}
IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
{
workButton().Content(winrt::box_value(L"Working..."));
// Enable the Pictures Library capability in the app manifest file.
StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };
m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);
IVectorView<StorageFile> filesInFolder{ co_await m_async };
workButton().Content(box_value(L"Done!"));
// Process the files in some way.
}
void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
{
if (m_async.Status() != AsyncStatus::Completed)
{
m_async.Cancel();
workButton().Content(winrt::box_value(L"Canceled"));
}
}
private:
IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...
Pour le côté implémentation de l’annulation, commençons par un exemple simple.
// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;
IAsyncAction ImplicitCancelationAsync()
{
while (true)
{
std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
co_await 1s;
}
}
IAsyncAction MainCoroutineAsync()
{
auto implicit_cancelation{ ImplicitCancelationAsync() };
co_await 3s;
implicit_cancelation.Cancel();
}
int main()
{
winrt::init_apartment();
MainCoroutineAsync().get();
}
Si vous exécutez l’exemple ci-dessus, vous verrez ImpliciteCancelationAsync imprimer un message par seconde pendant trois secondes, après quoi il se termine automatiquement suite à l’annulation. Cela fonctionne car, lors de la rencontre d’une co_await expression, une coroutine vérifie si elle a été annulée. Si c’est le cas, il court-circuite ; et si ce n’est pas le cas, il s’interrompt normalement.
L’annulation peut, bien sûr, se produire pendant que la coroutine est suspendue. Ce n’est que lorsque la coroutine reprend son exécution, ou atteint un autre co_await, qu’elle vérifiera si elle a été annulée. Le problème tient à une granularité de latence potentiellement trop grossière lors de la prise en compte de l’annulation.
Ainsi, une autre option consiste à vérifier explicitement si une annulation a été demandée dans votre coroutine. Mettez à jour l’exemple ci-dessus avec le code dans la liste ci-dessous. Dans ce nouvel exemple, ExplicitCancelationAsync récupère l’objet retourné par la fonction winrt ::get_cancellation_token et l’utilise pour vérifier régulièrement si la coroutine a été annulée. Tant qu’elle n’est pas annulée, la coroutine s’exécute en boucle indéfiniment ; dès qu’elle est annulée, la boucle et la fonction se terminent normalement. Le résultat est le même que l’exemple précédent, mais ici la sortie se produit explicitement, et sous contrôle.
IAsyncAction ExplicitCancelationAsync()
{
auto cancelation_token{ co_await winrt::get_cancellation_token() };
while (!cancelation_token())
{
std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
co_await 1s;
}
}
IAsyncAction MainCoroutineAsync()
{
auto explicit_cancelation{ ExplicitCancelationAsync() };
co_await 3s;
explicit_cancelation.Cancel();
}
...
En attendant winrt ::get_cancellation_token récupère un jeton d’annulation avec connaissance de l’IAsyncAction que la coroutine produit en votre nom. Vous pouvez utiliser l’opérateur d’appel de fonction sur ce jeton pour interroger l’état d’annulation — autrement dit, pour vérifier par sondage si une annulation a été demandée. Si vous effectuez une opération liée au calcul ou que vous effectuez une itération dans une collection volumineuse, il s’agit d’une technique raisonnable.
Enregistrer une fonction de rappel d’annulation
L'annulation du Windows Runtime ne transite pas automatiquement vers d'autres objets asynchrones. Mais — fonctionnalité introduite dans la version 10.0.17763.0 (Windows 10, version 1809) du kit de développement logiciel (SDK) Windows — vous pouvez enregistrer un rappel d’annulation. Il s’agit d’un hook préemptif par lequel l’annulation peut être propagée et permet d’intégrer des bibliothèques d’accès concurrentiel existantes.
Dans cet exemple de code suivant, NestedCoroutineAsync effectue le travail, mais il n’y a pas de logique d’annulation spéciale. CancelationPropagatorAsync est essentiellement un encapsuleur de la coroutine imbriquée ; cet encapsuleur propage l’annulation de manière préventive.
// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;
IAsyncAction NestedCoroutineAsync()
{
while (true)
{
std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
co_await 1s;
}
}
IAsyncAction CancelationPropagatorAsync()
{
auto cancelation_token{ co_await winrt::get_cancellation_token() };
auto nested_coroutine{ NestedCoroutineAsync() };
cancelation_token.callback([=]
{
nested_coroutine.Cancel();
});
co_await nested_coroutine;
}
IAsyncAction MainCoroutineAsync()
{
auto cancelation_propagator{ CancelationPropagatorAsync() };
co_await 3s;
cancelation_propagator.Cancel();
}
int main()
{
winrt::init_apartment();
MainCoroutineAsync().get();
}
CancelationPropagatorAsync enregistre une fonction lambda pour sa propre fonction de rappel d’annulation, puis attend (se suspend) jusqu’à ce que l’opération imbriquée se termine. Quand ou si CancellationPropagatorAsync est annulé, il propage l’annulation vers la coroutine imbriquée. Il n’est pas nécessaire de vérifier régulièrement s’il y a une demande d’annulation ; et l’annulation n’est pas non plus bloquée indéfiniment. Ce mécanisme est suffisamment flexible pour vous permettre de l’utiliser pour interagir avec une bibliothèque coroutine ou de concurrence qui ne connaît rien de C++/WinRT.
Création de rapports sur la progression
Si votre coroutine retourne IAsyncActionWithProgress ou IAsyncOperationWithProgress, vous pouvez récupérer l’objet retourné par la fonction winrt ::get_progress_token et l’utiliser pour renvoyer la progression à un gestionnaire de progression. Voici un exemple de code.
// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;
IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
auto progress{ co_await winrt::get_progress_token() };
co_await 1s;
double pi_so_far{ 3.1 };
progress.set_result(pi_so_far);
progress(0.2);
co_await 1s;
pi_so_far += 4.e-2;
progress.set_result(pi_so_far);
progress(0.4);
co_await 1s;
pi_so_far += 1.e-3;
progress.set_result(pi_so_far);
progress(0.6);
co_await 1s;
pi_so_far += 5.e-4;
progress.set_result(pi_so_far);
progress(0.8);
co_await 1s;
pi_so_far += 9.e-5;
progress.set_result(pi_so_far);
progress(1.0);
co_return pi_so_far;
}
IAsyncAction DoMath()
{
auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Progress([](auto const& sender, double progress)
{
std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
<< L"Value so far: " << sender.GetResults() << std::endl;
});
double pi{ co_await async_op_with_progress };
std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
std::wcout << L"Pi is approx.: " << pi << std::endl;
}
int main()
{
winrt::init_apartment();
DoMath().get();
}
Pour signaler la progression, appelez le jeton de progression avec la valeur de progression en tant qu’argument. Pour définir un résultat provisoire, utilisez la set_result() méthode sur le jeton de progression.
Note
La création de rapports de résultats provisoires nécessite C++/WinRT version 2.0.210309.3 ou ultérieure.
L’exemple ci-dessus choisit de définir un résultat provisoire pour chaque rapport de progression. Vous pouvez choisir de signaler des résultats provisoires à tout moment, le cas échéant. Il n’a pas besoin d’être couplé à un rapport de progression.
Note
Il n’est pas correct d’implémenter plusieurs gestionnaires d’achèvement pour une action ou une opération asynchrone. Vous pouvez avoir soit un seul délégué pour son événement d’achèvement, soit vous pouvez co_await le faire. Si vous avez les deux, la seconde échoue. L’un des deux types suivants de gestionnaires d’achèvement est approprié ; pas les deux pour le même objet asynchrone.
auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };
Pour plus d’informations sur les gestionnaires d’achèvement, consultez Types délégués pour les actions et opérations asynchrones.
Feu et oubli
Parfois, vous disposez d’une tâche qui peut être effectuée simultanément avec d’autres tâches, et vous n’avez pas besoin d’attendre que cette tâche se termine (aucun autre travail ne dépend de celui-ci), ni n’avez-vous besoin qu’il retourne une valeur. Dans ce cas, vous pouvez déclencher la tâche et l’oublier. Vous pouvez le faire en écrivant une coroutine dont le type de retour est winrt ::fire_and_forget (au lieu de l’un des types d’opérations asynchrones Windows Runtime, ou concurrency ::task).
// main.cpp
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace std::chrono_literals;
winrt::fire_and_forget CompleteInFiveSeconds()
{
co_await 5s;
}
int main()
{
winrt::init_apartment();
CompleteInFiveSeconds();
// Do other work here.
}
winrt ::fire_and_forget est également utile en tant que type de retour de votre gestionnaire d’événements lorsque vous devez effectuer des opérations asynchrones dans celui-ci. Voici un exemple (voir également Les références fortes et faibles en C++/WinRT).
winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.
auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
args.SetStorageFile(file);
// The destructor of unique_deferral completes the deferral here.
}
Le premier argument ( l’expéditeur) est laissé sans nom, car nous ne l’utilisons jamais. Pour cette raison, nous sommes sûrs de le laisser comme référence. Mais notez que args est passé par valeur. Consultez la section passage de paramètres ci-dessus .
En attente d’un descripteur du noyau
C++/WinRT fournit une fonction winrt ::resume_on_signal , que vous pouvez utiliser pour suspendre jusqu’à ce qu’un événement de noyau soit signalé. Vous devez vous assurer que l’identifiant reste valide jusqu’au retour de votre co_await resume_on_signal(h).
resume_on_signal elle-même ne peut pas le faire pour vous, car vous avez peut-être perdu la poignée même avant le démarrage de la resume_on_signal , comme dans ce premier exemple.
IAsyncAction Async(HANDLE event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle is not valid here.
}
Le descripteur HANDLE entrant n’est valide que jusqu’au retour de la fonction, et cette fonction (qui est une coroutine) retourne au premier point de suspension (le premier co_await dans ce cas). Lors de l’attente de DoWorkAsync, le contrôle est retourné à l’appelant, la pile d’appel de l’appelant est sortie de portée, et vous ne savez plus si le descripteur sera valide lorsque votre coroutine reprendra son exécution.
Techniquement, notre coroutine reçoit ses paramètres par valeur, comme il le devrait (voir Passage de paramètre ci-dessus ). Mais dans ce cas, nous devons aller plus loin afin que nous suivions l’esprit de ces conseils (plutôt que simplement la lettre). Nous devons transmettre une référence forte (en d’autres termes, propriété) avec le handle. Voici comment procéder.
IAsyncAction Async(winrt::handle event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle *is* valid here.
}
La transmission d’un winrt ::handle par valeur fournit une sémantique de propriété, ce qui garantit que le handle du noyau reste valide pendant la durée de vie de la coroutine.
Voici comment vous pourriez appeler cette coroutine.
namespace
{
winrt::handle duplicate(winrt::handle const& other, DWORD access)
{
winrt::handle result;
if (other)
{
winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
}
return result;
}
winrt::handle make_manual_reset_event(bool initialState = false)
{
winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
winrt::check_bool(static_cast<bool>(event));
return event;
}
}
IAsyncAction SampleCaller()
{
handle event{ make_manual_reset_event() };
auto async{ Async(duplicate(event)) };
::SetEvent(event.get());
event.close(); // Our handle is closed, but Async still has a valid handle.
co_await async; // Will wake up when *event* is signaled.
}
Vous pouvez passer une valeur de délai d’expiration à resume_on_signal, comme dans cet exemple.
winrt::handle event = ...
if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
puts("signaled");
}
else
{
puts("timed out");
}
Délais d’attente asynchrones simplifiés
C++/WinRT repose largement sur les coroutines C++. Leur effet sur la façon d’écrire du code concurrent est révolutionnaire. Cette section aborde les cas où les détails liés à l’asynchronisme n’ont pas d’importance et où tout ce que vous voulez, c’est obtenir le résultat immédiatement. Pour cette raison, l'implémentation de C++/WinRT de l'interface d'opération asynchrone IAsyncAction Windows Runtime a une fonction get, similaire à celle fournie par std ::future.
using namespace winrt::Windows::Foundation;
int main()
{
IAsyncAction async = ...
async.get();
puts("Done!");
}
La fonction get se bloque indéfiniment, tandis que l’objet asynchrone se termine. Les objets asynchrones ont tendance à avoir une durée de vie très courte, donc c’est souvent tout ce dont vous avez besoin.
Mais il existe des cas où cela n’est pas suffisant et que vous devez abandonner l’attente après un certain temps. L’écriture de ce code a toujours été possible, grâce aux blocs de construction fournis par la Windows Runtime. Mais maintenant C++/WinRT le rend beaucoup plus facile en fournissant la fonction wait_for . Il est également implémenté sur IAsyncAction, et de nouveau, il est similaire à celui fourni par std ::future.
using namespace std::chrono_literals;
int main()
{
IAsyncAction async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
puts("done");
}
}
Note
wait_for utilise std ::chrono ::d uration à l’interface, mais elle est limitée à une plage inférieure à ce que std ::chrono ::d uration fournit (environ 49,7 jours).
La wait_for dans cet exemple suivant attend environ cinq secondes, puis vérifie l’achèvement. Si la comparaison est favorable, vous savez que l’objet asynchrone s’est terminé correctement et que vous avez terminé. Si vous attendez un résultat, vous pouvez simplement suivre cela avec un appel à la méthode GetResults pour récupérer le résultat.
Note
wait_for et get sont mutuellement exclusifs (vous ne pouvez pas appeler les deux méthodes). Ils comptent chacun comme une attente, et les actions/opérations asynchrones de Windows Runtime ne prennent en charge qu’une seule attente.
int main()
{
IAsyncOperation<int> async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
printf("result %d\n", async.GetResults());
}
}
Étant donné que l’objet asynchrone est terminé, la méthode GetResults retourne immédiatement le résultat, sans attendre. Comme vous pouvez le voir, wait_for retourne l’état de l’objet asynchrone. Vous pouvez donc l’utiliser pour un contrôle plus précis, comme celui-ci.
switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
printf("result %d\n", async.GetResults());
break;
case AsyncStatus::Canceled:
puts("canceled");
break;
case AsyncStatus::Error:
puts("failed");
break;
case AsyncStatus::Started:
puts("still running");
break;
}
- N’oubliez pas que AsyncStatus ::Completed signifie que l’objet asynchrone s’est terminé correctement et que vous pouvez appeler la méthode GetResults pour récupérer tout résultat.
- AsyncStatus ::Canceled signifie que l’objet asynchrone a été annulé. Une annulation est généralement demandée par l’appelant. Il serait donc rare de gérer cet état. En règle générale, un objet asynchrone annulé est simplement ignoré. Vous pouvez appeler la méthode GetResults pour réactiver l’exception d’annulation si vous le souhaitez.
- AsyncStatus ::Error signifie que l’objet asynchrone a échoué d’une certaine façon. Vous pouvez appeler la méthode GetResults pour réactiver l’exception si vous le souhaitez.
- AsyncStatus ::Started signifie que l’objet asynchrone est toujours en cours d’exécution. Le modèle asynchrone de Windows Runtime n’autorise ni les attentes multiples ni plusieurs appelants en attente. Cela signifie que vous ne pouvez pas appeler wait_for dans une boucle. Si le délai d’attente est effectivement dépassé, il ne vous reste plus que quelques choix. Vous pouvez abandonner l’objet, ou vous pouvez interroger son état avant d’appeler la méthode GetResults pour récupérer n’importe quel résultat. Mais il est préférable d’ignorer l’objet à ce stade.
Une autre approche consiste à vérifier uniquement si l’état est Started, et à laisser GetResults gérer les autres cas.
if (async.wait_for(5s) == AsyncStatus::Started)
{
puts("timed out");
}
else
{
// will throw appropriate exception if in canceled or error state
auto results = async.GetResults();
}
Renvoyer un tableau de manière asynchrone
Vous trouverez ci-dessous un exemple de MIDL 3.0 qui génère l’erreur MIDL2025 : [msg]erreur de syntaxe [context] : > attendu ou, près de « [ ».
Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();
La raison est qu’il n’est pas valide d’utiliser un tableau comme argument de type de paramètre pour une interface paramétrable. Nous avons donc besoin d’un moyen moins évident d’atteindre l’objectif de transmettre de façon asynchrone un tableau à partir d’une méthode de classe runtime.
Vous pouvez renvoyer le tableau encapsulé dans un objet PropertyValue. Le code appelant le déballe ensuite. Voici un exemple de code que vous pouvez essayer en ajoutant la classe runtime SampleComponent à un projet Windows Runtime Component (C++/WinRT), puis en consommant cela à partir (par exemple) d'un projet Application vide, Empaquetée (WinUI 3 in Desktop).
// SampleComponent.idl
namespace MyComponentProject
{
runtimeclass SampleComponent
{
Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
};
}
// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
...
Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
{
co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
}
}
...
// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...
API importantes
- Interface IAsyncAction
- Interface IAsyncActionWithProgress<TProgress>
- interface IAsyncOperation<TResult>
- Interface IAsyncOperationWithProgress<TResult, TProgress>
- méthode SyndicationClient::RetrieveFeedAsync
- winrt ::fire_and_forget
- winrt::get_cancellation_token
- winrt::get_progress_token
- winrt::resume_foreground
Rubriques connexes
Windows developer