Programmation asynchrone en C++/CX

Remarque

Cette rubrique existe pour vous aider à gérer votre application C++/CX. Mais nous recommandons que vous utilisiez C++/WinRT pour de nouvelles applications. C++/WinRT est une projection de langage C++17 moderne entièrement standard pour les API Windows Runtime (WinRT), implémentée en tant que bibliothèque basée sur un fichier d’en-tête et conçue pour vous fournir un accès de première classe à l’API Windows moderne.

Cet article décrit la meilleure façon d’utiliser des méthodes asynchrones en extensions de composants Visual C++ (C++/CX) à l’aide de la classe task qui est définie dans l’espace de noms concurrency dans ppltasks.h.

Types asynchrones Windows Runtime

Windows Runtime propose un modèle bien défini pour appeler des méthodes asynchrones et fournit les types dont vous avez besoin pour consommer ces méthodes. Si vous n’êtes pas familiarisé avec le modèle asynchrone Windows Runtime, lisez la programmation asynchrone avant de lire le reste de cet article.

Bien que vous puissiez utiliser les API Windows Runtime asynchrones directement en C++, l’approche recommandée consiste à utiliser la classe de tâches et ses types et fonctions associés, qui sont contenus dans l’espace de noms d’accès concurrentiel et définis dans <ppltasks.h>. La concurrence::task est un type à usage général mais, lorsque le commutateur du compilateur /ZW, qui est requis pour les applications et composants plateforme Windows universelle (UWP), est utilisé, la classe de tâches encapsule les types asynchrones Windows Runtime afin qu’il soit plus facile de :

  • chaîner plusieurs opérations asynchrones et synchrones ensemble

  • gérer les exceptions dans les chaînes de tâches

  • effectuer une annulation dans les chaînes de tâches

  • vérifier que les tâches individuelles s’exécutent dans le contexte de thread ou le cloisonnement appropriés

Cet article fournit des conseils de base sur l’utilisation de la classe de tâches avec les API asynchrones Windows Runtime. Pour obtenir une documentation plus complète sur la tâche et ses méthodes associées, notamment create_task, consultez le parallélisme des tâches (runtime d’accès concurrentiel).

Utilisation d’une opération asynchrone à l’aide d’une tâche

L’exemple suivant montre comment utiliser la classe de tâches pour consommer une méthode asynchrone qui retourne une interface IAsyncOperation et dont l’opération produit une valeur. Les étapes de base sont les suivantes :

  1. Appelez la méthode create_task et transmettez-la à l’objet IAsyncOperation^.

  2. Appelez la tâche de fonction task::then, sur la tâche et fournissez une expression lambda qui sera invoquée lorsque l’opération asynchrone se termine.

#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{    
    //Call the *Async method that starts the operation.
    IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
        DeviceInformation::FindAllAsync();

    // Explicit construction. (Not recommended)
    // Pass the IAsyncOperation to a task constructor.
    // task<DeviceInformationCollection^> deviceEnumTask(deviceOp);

    // Recommended:
    auto deviceEnumTask = create_task(deviceOp);

    // Call the task's .then member function, and provide
    // the lambda to be invoked when the async operation completes.
    deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
    {       
        for(int i = 0; i < devices->Size; i++)
        {
            DeviceInformation^ di = devices->GetAt(i);
            // Do something with di...          
        }       
    }); // end lambda
    // Continue doing work or return...
}

La tâche créée et retournée par la fonction task::then est appelée continuation. L’argument d’entrée (dans ce cas) à l’expression lambda fournie par l’utilisateur est le résultat que l’opération de tâche produit lorsqu’elle se termine. Il s’agit de la même valeur que celle qui serait récupérée en appelant IAsyncOperation::GetResults si vous utilisiez directement l’interface IAsyncOperation.

La méthode task::then retourne immédiatement et son délégué ne s’exécute pas tant que le travail asynchrone n’est pas terminé. Dans cet exemple, si l’opération asynchrone provoque la levée d’une exception ou se termine par l’état annulé à la suite d’une demande d’annulation, la continuation n’est jamais exécutée. Nous décrirons plus tard comment écrire des continuations qui s’exécutent même si la tâche précédente a été annulée ou a échoué.

Bien que vous déclarez la variable de tâche sur la pile locale, elle gère sa durée de vie afin qu’elle ne soit pas supprimée tant que toutes ses opérations ne sont pas terminées et que toutes les références à celle-ci sortent de l’étendue, même si la méthode retourne avant la fin des opérations.

Création d’une chaîne de tâches

Dans la programmation asynchrone, il est courant de définir une séquence d’opérations, également appelée chaînes de tâches, dans lesquelles chaque continuation s’exécute uniquement lorsque la précédente se termine. Dans certains cas, la tâche précédente (ou antécédent) produit une valeur que la continuation accepte comme entrée. En utilisant la méthode task::then, vous pouvez créer des chaînes de tâches de manière intuitive et simple ; la méthode retourne une tâche<T>T est le type de retour de la fonction lambda. Vous pouvez composer plusieurs continuations dans une chaîne de tâches : myTask.then(…).then(…).then(…);

Les chaînes de tâches sont particulièrement utiles lorsqu’une continuation crée une opération asynchrone ; une telle tâche est appelée tâche asynchrone. L’exemple suivant illustre une chaîne de tâches qui a deux continuations. La tâche initiale acquiert le descripteur dans un fichier existant et, une fois cette opération terminée, la première continuation démarre une nouvelle opération asynchrone pour supprimer le fichier. Une fois cette opération terminée, la deuxième continuation s’exécute et génère un message de confirmation.

#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{    
    using namespace Windows::Storage;
    StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
    auto getFileTask = create_task(localFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {       
        return storageFileSample->DeleteAsync();
    }).then([](void) {
        OutputDebugString(L"File deleted.");
    });
}

L'exemple précédent illustre les quatre points importants :

  • La première continuation convertit l’objet IAsyncAction^ en une tâche<void> et retourne la tâche.

  • La deuxième continuation n’effectue aucune gestion des erreurs, et prend donc void et non la tâche<void> comme entrée. Il s’agit d’une continuation basée sur des valeurs.

  • La deuxième continuation ne s’exécute pas tant que l’opération DeleteAsync n’est pas terminée.

  • Étant donné que la deuxième continuation est basée sur des valeurs, si l’opération démarrée par l’appel à DeleteAsync lève une exception, la deuxième continuation ne s’exécute pas du tout.

Notez que la création d’une chaîne de tâches n’est qu’une des façons d’utiliser la classe de tâches pour composer des opérations asynchrones. Vous pouvez également composer des opérations à l’aide des opérateurs de jointure et de choix && et ||. Pour plus d'informations, consultez Parallélisme des tâches (runtime d’accès concurrentiel).

Types de retour de fonction lambda et types de retour de tâche

Dans une continuation de tâche, le type de retour de la fonction lambda est enveloppé dans un objet de tâche. Si l’expression lambda retourne un double, le type de la tâche de continuation est tâche<double>. Toutefois, l’objet de tâche est conçu pour qu’il ne produise pas de types de retour imbriqués inutilement. Si une expression lambda retourne un IAsyncOperation<SyndicationFeed^>^, la continuation renvoie une tâche<SyndicationFeed^>, et non une tâche<tâche<SyndicationFeed^>> ou la tâche<IAsyncOperation<SyndicationFeed^>^>^. Ce processus est appelé désenveloppement asynchrone et garantit également que l’opération asynchrone à l’intérieur de la continuation se termine avant l’appel de la continuation suivante.

Dans l’exemple précédent, notez que la tâche retourne une tâche<void> même si son lambda a renvoyé un objet IAsyncInfo. Le tableau suivant récapitule les conversions de type qui se produisent entre une fonction lambda et la tâche englobante :

Type de retour lambda type de retour .then
TResult task<TResult>
IAsyncOperation<TResult>^ task<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ task<TResult>
IAsyncAction^ task<void>
IAsyncActionWithProgress<TProgress>^ task<void>
task<TResult> task<TResult>

Annulation des tâches

Il est souvent judicieux de donner à l’utilisateur la possibilité d’annuler une opération asynchrone. Dans certains cas, vous devrez peut-être annuler une opération par programmation en dehors de la chaîne de tâches. Bien que chaque type de retour *Async ait une méthode Cancel qu’il hérite d’IAsyncInfo, il est maladroit de l’exposer à des méthodes extérieures. La meilleure façon de prendre en charge l’annulation dans une chaîne de tâches consiste à utiliser un cancellation_token_source pour créer un cancellation_token, puis de transmettre le jeton au constructeur de la tâche initiale. Si une tâche asynchrone est créée avec un jeton d’annulation et [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017>true) est appelée, la tâche appelle automatiquement Cancel sur l’opération IAsync* et transmet la demande d’annulation à sa chaîne de continuation. Le pseudocode suivant illustre l’approche de base.

//Class member:
cancellation_token_source m_fileTaskTokenSource;

// Cancel button event handler:
m_fileTaskTokenSource.cancel();

// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
                                m_fileTaskTokenSource.get_token());
//getFileTask2.then ...

Lorsqu’une tâche est annulée, une exception task_canceled est propagée vers le bas de la chaîne de tâches. Les continuations basées sur des valeurs ne s’exécutent pas, mais les continuations basées sur des tâches entraînent la levée de l’exception lorsque task::get est appelée. Si vous avez une continuation de gestion des erreurs, assurez-vous qu’elle intercepte explicitement l’exception task_canceled. (Cette exception n’est pas dérivée de Platform::Exception.)

L'annulation est coopérative. Si votre continuation effectue un travail de longue durée au-delà de l’appel d’une méthode UWP, il vous incombe de vérifier l’état du jeton d’annulation régulièrement et d’arrêter l’exécution si elle est annulée. Après avoir nettoyé toutes les ressources qui ont été allouées dans la continuation, appelez cancel_current_task pour annuler cette tâche et propager l’annulation à toutes les continuations basées sur des valeurs qui le suivent. Voici un autre exemple : vous pouvez créer une chaîne de tâches qui représente le résultat d’une opération FileSavePicker. Si l’utilisateur choisit le bouton Annuler, la méthode IAsyncInfo::Cancel n’est pas appelée. Au lieu de cela, l’opération réussit, mais retourne nullptr. La continuation peut tester le paramètre d’entrée et appeler cancel_current_task si l’entrée est nullptr.

Pour plus d’informations, voir Annulation dans la bibliothèque de modèles parallèles

Gestion des erreurs dans une chaîne de tâches

Si vous souhaitez qu’une continuation s’exécute même si l’antécédent a été annulé ou a levé une exception, faites de la continuation une continuation basée sur des tâches en spécifiant l’entrée à sa fonction lambda en tant que tâche<TResult> ou tâche<void> si le lambda de la tâche antécédente retourne un IAsyncAction^.

Pour gérer les erreurs et l’annulation dans une chaîne de tâches, vous n’avez pas besoin d’effectuer chaque continuation basée sur des tâches ou de placer chaque opération susceptible lever une exception dans un bloc try…catch. Au lieu de cela, vous pouvez ajouter une continuation basée sur des tâches à la fin de la chaîne et y gérer toutes les erreurs. Toute exception—qui inclut une exception task_canceled—se propage vers le bas de la chaîne de tâches et contourne toutes les continuations basées sur des valeurs, afin de pouvoir la gérer dans la continuation basée sur les tâches de gestion des erreurs. Nous pouvons réécrire l’exemple précédent pour utiliser une continuation basée sur des tâches de gestion des erreurs :

#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{    
    using namespace Windows::Storage;
    using namespace concurrency;

    StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
    auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample)
    {       
        return storageFileSample->DeleteAsync();
    })

    .then([](task<void> t)
    {

        try
        {
            t.get();
            // .get() didn' t throw, so we succeeded.
            OutputDebugString(L"File deleted.");
        }
        catch (Platform::COMException^ e)
        {
            //Example output: The system cannot find the specified file.
            OutputDebugString(e->Message->Data());
        }

    });
}

Dans une continuation basée sur des tâches, nous appelons la tâche de fonction tas::get pour obtenir les résultats de la tâche. Nous devons toujours appeler la task::get même si l’opération était un IAsyncAction qui ne produit aucun résultat, car task::get obtient également toutes les exceptions qui ont été transférées vers la tâche. Si la tâche d’entrée stocke une exception, elle est levée à l’appel à task::get. Si vous n’appelez pas task::get, ou si vous n’utilisez pas de continuation basée sur des tâches à la fin de la chaîne ou ne interceptez pas le type d’exception qui a été levée, une unobserved_task_exception est levée lorsque toutes les références à la tâche ont été supprimées.

Interceptez uniquement les exceptions que vous pouvez gérer. Si votre application rencontre une erreur à partir de laquelle vous ne pouvez pas récupérer, il est préférable de laisser l’application se bloquer que de continuer à s’exécuter dans un état inconnu. En général, n’essayez pas d’attraper la unobserved_task_exception elle-même. Cette exception est principalement destinée à des fins de diagnostic. Lorsque unobserved_task_exception est levée, elle indique généralement un bogue dans le code. Souvent, la cause est une exception qui doit être gérée, ou une exception irrécupérable provoquée par une autre erreur dans le code.

Gestion du contexte de thread

L’IU d’une application UWP s’exécute dans un thread unique cloisonné (STA). Tâche dont l’expression lambda retourne un IAsyncAction ou IAsyncOperation prend en charge le cloisonnement. Si la tâche est créée dans un STA, toutes ses continuations s’y exécutent également par défaut, sauf indication contraire. En d’autres termes, la chaîne de tâches entière hérite de la prise en charge du cloisonnement de la tâche parente. Ce comportement permet de simplifier les interactions avec les contrôles d’interface utilisateur, qui ne peuvent être accessibles qu’à partir du STA.

Par exemple, dans une application UWP, dans la fonction membre d’une classe qui représente une page XAML, vous pouvez remplir un contrôle ListBox à partir d’une méthode task::then, sans avoir à utiliser l’objet Dispatcher.

#include <ppltasks.h>
void App::SetFeedText()
{    
    using namespace Windows::Web::Syndication;
    using namespace concurrency;
    String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
    SyndicationClient^ client = ref new SyndicationClient();
    auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));

    create_task(feedOp).then([this]  (SyndicationFeed^ feed)
    {
        m_TextBlock1->Text = feed->Title->Text;
    });
}

Si une tâche ne retourne pas d’IAsyncAction ou IAsyncOperation, elle ne prend pas en charge le cloisonnement et, par défaut, ses continuations sont exécutées sur le premier thread d’arrière-plan disponible.

Vous pouvez remplacer le contexte de thread par défaut pour un type de tâche à l’aide de la surcharge de task::then qui prend une task_continuation_context. Par exemple, dans certains cas, il peut être souhaitable de planifier la continuation d’une tâche prenant en charge le cloisonnement sur une conversation d’arrière-plan. Dans ce cas, vous pouvez transmettre task_continuation_context::use_arbitrary pour planifier le travail de la tâche sur le thread disponible suivant dans un cloisonnement multithread. Cela peut améliorer les performances de la continuation, car son travail n’a pas besoin d’être synchronisé avec d’autres travaux qui se produisent sur le thread d’interface utilisateur.

L’exemple suivant montre quand il est utile de spécifier l’option task_continuation_context::use_arbitrary, et montre également comment le contexte de continuation par défaut est utile pour synchroniser les opérations simultanées sur des collections non thread-safe. Dans ce code, nous parcourons une liste d’URL pour les flux RSS et, pour chaque URL, nous commençons une opération asynchrone pour récupérer les données de flux. Nous ne pouvons pas contrôler l’ordre dans lequel les flux sont récupérés, et nous ne nous soucions pas vraiment. Lorsque chaque opération RetrieveFeedAsync se termine, la première continuation accepte l’objet SyndicationFeed^ et l’utilise pour initialiser un objet défini par FeedData^ l’application. Étant donné que chacune de ces opérations est indépendante des autres, nous pouvons potentiellement accélérer les choses en spécifiant le contexte de continuation task_continuation_context::use_arbitrary. Toutefois, après l’initialisation de chaque FeedData objet, nous devons l’ajouter à un vector, qui n’est pas une collection thread-safe. Par conséquent, nous créons une continuation et spécifions [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017>true) pour vous assurer que tous les appels à Append se produisent dans le même contexte ASTA (Application Single-Threaded Apartment). Étant donné que task_continuation_context::use_default est le contexte par défaut, nous n’avons pas à le spécifier explicitement, mais nous le faisons ici pour la clarté.

#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
                using namespace concurrency;
    SyndicationClient^ client = ref new SyndicationClient();

    std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
    {
        // Create the async operation. feedOp is an
        // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
        // but we don't handle progress in this example.

        auto feedUri = ref new Uri(ref new String(url.c_str()));
        auto feedOp = client->RetrieveFeedAsync(feedUri);

        // Create the task object and pass it the async operation.
        // SyndicationFeed^ is the type of the return value
        // that the feedOp operation will eventually produce.

        // Then, initialize a FeedData object by using the feed info. Each
        // operation is independent and does not have to happen on the
        // UI thread. Therefore, we specify use_arbitrary.
        create_task(feedOp).then([this]  (SyndicationFeed^ feed) -> FeedData^
        {
            return GetFeedData(feed);
        }, task_continuation_context::use_arbitrary())

        // Append the initialized FeedData object to the list
        // that is the data source for the items collection.
        // This all has to happen on the same thread.
        // By using the use_default context, we can append
        // safely to the Vector without taking an explicit lock.
        .then([feedList] (FeedData^ fd)
        {
            feedList->Append(fd);
            OutputDebugString(fd->Title->Data());
        }, task_continuation_context::use_default())

        // The last continuation serves as an error handler. The
        // call to get() will surface any exceptions that were raised
        // at any point in the task chain.
        .then( [this] (task<void> t)
        {
            try
            {
                t.get();
            }
            catch(Platform::InvalidArgumentException^ e)
            {
                //TODO handle error.
                OutputDebugString(e->Message->Data());
            }
        }); //end task chain

    }); //end std::for_each
}

Les tâches imbriquées, qui sont de nouvelles tâches créées à l’intérieur d’une continuation, n’héritent pas de la prise en charge du cloisonnement de la tâche initiale.

Gestion des mises à jour de progression

Les méthodes qui prennent en charge IAsyncOperationWithProgress ou IAsyncActionWithProgress fournissent régulièrement des mises à jour de progression pendant que l’opération est en cours, avant qu’elle ne se termine. Les rapports de progression sont indépendants de la notion de tâches et de continuations. Vous fournissez simplement le délégué pour la propriété de l’objet Progress. Une utilisation typique du délégué consiste à mettre à jour une barre de progression dans l’interface utilisateur.