Concurrence et opérations asynchrones avec C++/WinRT

Important

Cette rubrique présente les concepts de coroutines et co_await, que nous vous recommandons d’utiliser à la fois dans votre interface utilisateur et dans vos applications non-interface utilisateur. Par souci de simplicité, la plupart des exemples de code de cette rubrique introductive utilisent des projets Application console Windows (C++/WinRT). Les exemples de code qui suivent dans cette rubrique utilisent bien des coroutines, mais par commodité, les exemples d’application console continuent eux aussi d’utiliser l’appel bloquant à la fonction get juste avant de quitter, afin que l’application ne se ferme pas avant d’avoir fini d’afficher sa sortie. Vous ne ferez pas cela (appeler la fonction bloquante get) depuis un thread d’interface utilisateur. Au lieu de cela, vous allez utiliser l’instruction co_await . Les techniques que vous utiliserez dans vos applications d’interface utilisateur sont décrites dans la rubrique Accès concurrentiel avancé et synchronisation.

Cette rubrique d’introduction présente certaines des façons dont vous pouvez créer et utiliser des objets asynchrones Windows Runtime avec C++/WinRT. Après avoir lu cette rubrique, en particulier pour les techniques que vous utiliserez dans vos applications d’interface utilisateur, consultez également Accès concurrentiel avancé et asynchronité.

Opérations asynchrones et fonctions « Async » de Windows Runtime

Toute API Windows Runtime qui a le potentiel de prendre plus de 50 millisecondes pour se terminer est implémentée en tant que fonction asynchrone (avec un nom se terminant par « Async »). L’implémentation d’une fonction asynchrone lance le travail sur un autre thread et retourne immédiatement avec un objet qui représente l’opération asynchrone. Une fois l’opération asynchrone terminée, cet objet retourné contient n’importe quelle valeur résultant du travail. L’espace de noms Windows Runtime Windows ::Foundation contient quatre types d’objet d’opération asynchrone.

Chacun de ces types d’opérations asynchrones est projeté dans un type correspondant dans l’espace de noms Winrt ::Windows ::Foundation C++/WinRT. C++/WinRT contient également un struct d’adaptateur Await interne. Vous ne l’utilisez pas directement, mais, grâce à ce struct, vous pouvez écrire une instruction pour attendre de manière coopérative le résultat d’une co_await fonction qui retourne l’un de ces types d’opérations asynchrones. Et vous pouvez créer vos propres coroutines qui retournent ces types.

Un exemple de fonction Windows asynchrone est SyndicationClient ::RetrieveFeedAsync, qui retourne un objet d’opération asynchrone de type IAsyncOperationWithProgress<TResult, TProgress>.

Examinons quelques façons — d’abord de manière bloquante, puis de manière non bloquante — d’utiliser C++/WinRT pour appeler une API telle que celle-ci. Pour illustrer les idées de base, nous allons utiliser un projet d'application console Windows (C++/WinRT) dans les exemples de code suivants. Les techniques plus appropriées pour une application d’interface utilisateur sont abordées dans l’accès concurrentiel avancé et l’asynchronité.

Bloquer le thread appelant

L’exemple de code ci-dessous reçoit un objet d’opération asynchrone de RetrieveFeedAsync et appelle get sur cet objet pour bloquer le thread appelant jusqu’à ce que les résultats de l’opération asynchrone soient disponibles.

Si vous souhaitez copier-coller cet exemple directement dans le fichier de code source principal d’un projet d’application console Windows (C++/WinRT), commencez par définir Not Using Precompiled Headers in project properties.

// main.cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

L’appel à get facilite l’écriture du code et est idéal pour les applications console ou les fils d’exécution en arrière-plan, lorsque vous ne souhaitez peut-être pas utiliser une coroutine pour quelque raison que ce soit. Mais il n’est pas simultané ni asynchrone, il n’est donc pas approprié pour un thread d’interface utilisateur (et une assertion se déclenche dans des builds non optimisées si vous tentez de l’utiliser sur un). Pour éviter d’empêcher les threads de système d’exploitation d’effectuer d’autres tâches utiles, nous avons besoin d’une technique différente.

Écrire une coroutine

C++/WinRT intègre les coroutines C++ dans le modèle de programmation pour fournir un moyen naturel d’attendre de manière coopérative un résultat. Vous pouvez créer votre propre opération asynchrone du Windows Runtime en écrivant une coroutine. Dans l’exemple de code ci-dessous, ProcessFeedAsync est la coroutine.

Note

La fonction get existe sur le type de projection C++/WinRT winrt ::Windows ::Foundation ::IAsyncAction. Vous pouvez donc appeler la fonction à partir de n’importe quel projet C++/WinRT. Vous ne trouverez pas la fonction répertoriée en tant que membre de l’interface IAsyncAction, car get ne fait pas partie de la surface de l’interface binaire d’application (ABI) du type Windows Runtime réel IAsyncAction.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    PrintFeed(syndicationFeed);
}

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

Une coroutine est une fonction qui peut être suspendue et reprise. Dans la coroutine ProcessFeedAsync ci-dessus, lorsque l’instruction co_await est atteinte, la coroutine lance de façon asynchrone l’appel RetrieveFeedAsync , puis elle se suspend immédiatement et retourne le contrôle à l’appelant (qui est principal dans l’exemple ci-dessus). main peut ensuite continuer à faire du travail pendant que le flux est récupéré et imprimé. Lorsque cela est terminé (lorsque l’appel RetrieveFeedAsync se termine), la coroutine ProcessFeedAsync reprend à l’instruction suivante.

Vous pouvez intégrer une coroutine dans d’autres coroutines. Ou vous pouvez appeler get pour bloquer et attendre son exécution (et obtenir le résultat s’il y en a un). Vous pouvez également le transmettre à un autre langage de programmation qui prend en charge le Windows Runtime.

Il est également possible de gérer les événements terminés et/ou de progression des actions et opérations asynchrones à l’aide de délégués. Pour plus d’informations et pour obtenir des exemples de code, consultez Types délégués pour les actions et opérations asynchrones.

Comme vous pouvez le voir, dans l’exemple de code ci-dessus, nous continuons à utiliser l’appel bloquant à la fonction get juste avant de quitter main. Mais c’est uniquement pour éviter que l’application ne se ferme avant d’avoir fini d’imprimer sa sortie.

Retourner de façon asynchrone un type de Windows Runtime

Dans cet exemple suivant, nous encapsulons un appel à RetrieveFeedAsync, pour un URI spécifique, pour nous donner une fonction RetrieveBlogFeedAsync qui retourne de façon asynchrone un SyndicationFeed.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

Dans l’exemple ci-dessus, RetrieveBlogFeedAsync retourne un IAsyncOperationWithProgress, qui a à la fois la progression et une valeur de retour. Nous pouvons effectuer d’autres tâches pendant que RetrieveBlogFeedAsync effectue sa tâche et récupère le flux. Ensuite, nous appelons get sur cet objet d’opération asynchrone pour bloquer, attendre qu’il se termine, puis obtenir les résultats de l’opération.

Si vous renvoyez un type Windows Runtime de manière asynchrone, vous devez renvoyer un IAsyncOperation<TResult> ou un IAsyncOperationWithProgress<TResult, TProgress>. Toute classe d’exécution propre ou tierce est admissible, de même que tout type pouvant être passé à une fonction du Windows Runtime ou renvoyé par celle-ci (par exemple, int ou winrt::hstring). Le compilateur signalera l’erreur « T doit être de type WinRT » si vous essayez d’utiliser l’un de ces types d’opérations asynchrones avec un type qui n’est pas un type Windows Runtime.

Si une coroutine n’a pas au moins une co_await instruction, pour pouvoir se qualifier comme coroutine, elle doit avoir au moins une ou une co_returnco_yield instruction. Il y aura des cas où votre coroutine peut retourner une valeur sans introduire d’asynchronie, et par conséquent, sans blocage ni changement de contexte. Voici un exemple qui effectue cette opération (la seconde et les fois suivantes qu’elle est appelée) en mettant en cache une valeur.

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

Retourner de manière asynchrone un type autre qu’un type Windows Runtime

Si vous renvoyez de manière asynchrone un type qui n’est pas un type Windows Runtime, vous devez alors renvoyer un concurrency::task de la bibliothèque Parallel Patterns Library (PPL). Nous vous recommandons concurrency ::task , car elle vous offre de meilleures performances (et une meilleure compatibilité à l’avenir) que std ::future .

Tip

Si vous incluez <pplawait.h>, vous pouvez utiliser concurrency ::task comme type coroutine.

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

Passage de paramètres

Pour les fonctions synchrones, vous devez utiliser des paramètres const& par défaut. Cela évite la surcharge des copies (qui impliquent le comptage des références, et cela signifie des incréments et décréments interblocés).

// Synchronous function.
void DoWork(Param const& value);

Mais vous pouvez rencontrer des problèmes si vous passez un paramètre de référence à une coroutine.

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

Dans une coroutine, l’exécution est synchrone jusqu’au premier point de suspension, où le contrôle est rendu à l’appelant et le cadre d’appel sort de portée. Lorsque l’exécution de la coroutine reprend, il peut être arrivé n’importe quoi à la valeur source à laquelle fait référence un paramètre de référence. Du point de vue de la coroutine, un paramètre de référence a une durée de vie incontrôlée. Par conséquent, dans l’exemple ci-dessus, on peut accéder sans risque à value jusqu’à co_await, mais pas après. Dans le cas où value est détruit par l’appelant, tenter d’y accéder dans la coroutine après cela entraîne une corruption de la mémoire. Nous ne pouvons pas non plus transmettre la valeur à DoOtherWorkAsync en toute sécurité s’il existe un risque que cette fonction s’interrompe à son tour, puis essayons d’utiliser la valeur une fois qu’elle reprend.

Pour que les paramètres puissent être utilisés en toute sécurité après suspension et reprise, vos coroutines doivent, par défaut, utiliser le passage par valeur afin de garantir une capture par valeur et d’éviter les problèmes de durée de vie. Les cas où vous pouvez dévier de ces conseils parce que vous êtes certain qu’il est sûr de le faire sera rare.

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

La transmission par valeur nécessite que l’argument soit peu coûteux pour déplacer ou copier ; et c’est généralement le cas pour un pointeur intelligent.

On peut aussi soutenir que, sauf si vous souhaitez déplacer la valeur, passer par une valeur constante est une bonne pratique. Elle n’aura aucun effet sur la valeur source à partir de laquelle vous effectuez une copie, mais elle rend l’intention claire et vous aide si vous modifiez par inadvertance la copie.

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

Consultez également les tableaux et vecteurs Standard, qui traitent de la façon de passer un vecteur standard dans un appelé asynchrone.

Si vous ne pouvez pas modifier la signature de votre coroutine, mais que vous pouvez modifier l’implémentation, vous pouvez effectuer une copie locale avant la première co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Si copier Param est coûteux, extrayez uniquement les éléments dont vous avez besoin avant le premier co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

Accéder en toute sécurité au pointeur this dans une coroutine membre d’une classe

Consultez les références fortes et faibles en C++/WinRT.

API importantes