Partager via


Annulation dans la bibliothèque de modèles parallèles

Ce document explique le rôle de l'annulation dans la Bibliothèque de modèles parallèles (PPL, Parallel Patterns Library), comment annuler le travail parallèle et comment déterminer quand le travail parallèle est annulé.

Notes

Le runtime utilise la gestion des exceptions pour implémenter l'annulation.Par conséquent, vous ne devez ni intercepter, ni gérer ces exceptions dans votre code.De plus, nous vous recommandons d'écrire du code sécurisé du point de vue des exceptions dans les corps de fonction de vos tâches.Par exemple, vous pouvez utiliser le modèle RAII (Resource Acquisition Is Initialization) pour vérifier que les ressources sont gérées correctement lorsqu'une exception est levée dans le corps d'une tâche.Pour obtenir un exemple complet utilisant le modèle RAII pour nettoyer une ressource dans une tâche annulable, consultez Procédure pas à pas : suppression de travail d'un thread d'interface utilisateur.

Points clés

  • L'annulation est coopérative et implique la coordination entre le code qui demande l'annulation et la tâche qui répond à l'annulation.

  • Si possible, utilisez des jetons d'annulation pour annuler le travail. La classe concurrency::cancellation_token définit un jeton d'annulation.

  • Lorsque vous utilisez des jetons d'annulation, utilisez la méthode concurrency::cancellation_token_source::cancel pour initialiser l'annulation et les méthodes concurrency::is_task_cancellation_requested et concurrency::cancel_current_task pour répondre à annulation.

  • L'annulation ne se produit pas immédiatement. Bien qu'un nouveau travail ne soit pas démarré si une tâche ou un ensemble de tâches est annulée, le travail actif doit vérifier et répondre à l'annulation.

  • Un prolongement basé sur la valeur hérite du jeton d'annulation de la tâche antécédente. Un prolongement basé sur la tâche n'hérite jamais de jeton de la tâche antécédente.

  • Utilisez la méthode concurrency::cancellation_token::none lorsque vous appelez un constructeur ou une fonction qui prend un objet cancellation_token mais vous ne voulez pas que l'opération soit annulable. En outre, si vous ne transmettez pas un jeton d'annulation au constructeur concurrency::task ou à la fonction concurrency::create_task, cette tâche ne peut pas être annulable.

Dans ce document

  • Arborescences de travail parallèle

  • Annulation de tâches parallèles

    • Utilisation d'un jeton d'annulation pour annuler le travail parallèle

    • Utilisation de la méthode cancel pour annuler du travail parallèle

    • Utilisation d'exceptions pour annuler du travail parallèle

  • Annulation d'algorithmes parallèles

  • Quand ne pas utiliser l'annulation

Arborescences de travail parallèle

La bibliothèque PPL utilise des tâches et des groupes de tâches pour gérer les tâches affinées et les calculs. Vous pouvez imbriquer des groupes de tâches pour former des arborescences de travail parallèle. L'illustration suivante montre une arborescence de travail parallèle. Dans cette illustration, tg1 et tg2 représentent des groupes de tâches ; t1, t2, t3, t4, et t5 représentent le travail que des groupes de tâches effectuent.

Arborescence de travail parallèle

L'exemple suivant montre le code qui est obligatoire pour créer l'arborescence dans l'illustration. Dans cet exemple, tg1 et tg2 sont des objets concurrency::structured_task_group. t1, t2, t3, t4 et t5 sont des objets concurrency::task_handle.

// task-tree.cpp 
// compile with: /c /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

void create_task_tree()
{   
   // Create a task group that serves as the root of the tree.
   structured_task_group tg1;

   // Create a task that contains a nested task group.
   auto t1 = make_task([&] {
      structured_task_group tg2;

      // Create a child task.
      auto t4 = make_task([&] {
         // TODO: Perform work here.
      });

      // Create a child task.
      auto t5 = make_task([&] {
         // TODO: Perform work here.
      });

      // Run the child tasks and wait for them to finish.
      tg2.run(t4);
      tg2.run(t5);
      tg2.wait();
   });

   // Create a child task.
   auto t2 = make_task([&] {
      // TODO: Perform work here.
   });

   // Create a child task.
   auto t3 = make_task([&] {
      // TODO: Perform work here.
   });

   // Run the child tasks and wait for them to finish.
   tg1.run(t1);
   tg1.run(t2);
   tg1.run(t3);
   tg1.wait();   
}

Vous pouvez également utiliser la classe concurrency::task_group pour créer une structure similaire de travail. La classe concurrency::task prend également en charge la notion d'un arbre de travail. Toutefois, l'arborescence de task est une arborescence de dépendance. Dans l'arborescence de task, les prochains travaux terminent au travail actuel. Dans une arborescence des tâches, le travail interne se termine avant le travail externe. Pour plus d'informations sur les différences entre les tâches et les groupe de tâches, consultez Parallélisme des tâches (runtime d'accès concurrentiel).

[Premières]

Annulation de tâches parallèles

Il y a plusieurs façons d'annuler du travail parallèle. La méthode privilégiée consiste à utiliser un jeton d'annulation. Les groupe de tâches appuient aussi la méthode concurrency::task_group::cancel et la méthode concurrency::structured_task_group::cancel. La dernier moyen consiste à lever une exception dans le corps d'une fonction de travail de tâche. Quelle que soit la méthode que vous choisissez, comprenez que l'annulation ne se produit pas immédiatement. Bien qu'un nouveau travail n'est pas démarré si une tâche ou un ensemble de tâches est annulée, le travail actif doit vérifier et répondre à l'annulation.

Pour obtenir plus d'exemples d'annulation de tâches parallèles, consultez Procédure pas à pas : connexion à l'aide de tâches et de requêtes HTTP XML, Comment : utiliser l'annulation pour rompre une boucle parallèle et Comment : utiliser la gestion des exceptions pour rompre une boucle parallèle.

Utilisation d'un jeton d'annulation pour annuler le travail parallèle

Les classes task, task_group et structured_task_group prennent en charge l'annulation via l'utilisation de jetons d'annulation. Le PPL définit les classes concurrency::cancellation_token_source et concurrency::cancellation_token à cet effet. Lorsque vous utilisez un jeton d'annulation pour annuler un processus, le runtime ne démarre pas le nouveau processus qui souscrit à ce jeton. Un processus qui est déjà actif peut contrôler son jeton d'annulation et s'arrêter lorsqu'il le peut.

Pour initialiser une annulation, appelez la méthode concurrency::cancellation_token_source::cancel. Vous réagissez à l'annulation de manières suivantes :

  • Pour les objets task, utilisez les fonctions concurrency::is_task_cancellation_requested et concurrency::cancel_current_task. cancel_current_task annule la tâche actuelle et toutes ses prolongements basés sur la valeur. (Il n'annule pas le jeton d'annulation associé à la tâche ou ses prolongements.)

  • Pour les groupes de tâches et les algorithmes parallèles, utilisez la fonction concurrency::is_current_task_group_canceling pour détecter annulation et retourner le plus tôt possible du corps de la tâche lorsque cette fonction retourne true. (N'appelez pas cancel_current_task d'un groupe de tâches.)

L'exemple suivant montre le modèle de base pour l'annulation de la tâche. Le corps de la tâche vérifie occasionnellement l'annulation dans une boucle.

// task-basic-cancellation.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

bool do_work()
{
    // Simulate work.
    wcout << L"Performing work..." << endl;
    wait(250);
    return true;
}

int wmain()
{
    cancellation_token_source cts;
    auto token = cts.get_token();

    wcout << L"Creating task..." << endl;

    // Create a task that performs work until it is canceled.
    auto t = create_task([]
    {
        bool moreToDo = true;
        while (moreToDo)
        {
            // Check for cancellation. 
            if (is_task_cancellation_requested())
            {
                // TODO: Perform any necessary cleanup here... 

                // Cancel the current task.
                cancel_current_task();
            }
            else 
            {
                // Perform work.
                moreToDo = do_work();
            }
        }
    }, token);

    // Wait for one second and then cancel the task.
    wait(1000);

    wcout << L"Canceling task..." << endl;
    cts.cancel();

    // Wait for the task to cancel.
    wcout << L"Waiting for task to complete..." << endl;
    t.wait();

    wcout << L"Done." << endl;
}

/* Sample output:
    Creating task...
    Performing work...
    Performing work...
    Performing work...
    Performing work...
    Canceling task...
    Waiting for task to complete...
    Done.
*/

La fonction cancel_current_task  lève; par conséquent, vous n'avez pas besoin de retourner explicitement la boucle en cours ou la fonction.

Conseil

Vous pouvez alternativement appeler la fonction concurrency::interruption_point au lieu de is_task_cancellation_requested et de cancel_current_task.

Il est important d'appeler cancel_current_task lorsque vous répondez à l'annulation car il passe la tâche à l'état d'annulation. Si vous retournez tôt au lieu d'appeler cancel_current_task, les transitions d'opération à l'état arrêté et toutes les continuations basées sur la valeur sont exécutées.

Avertissement

N'enlevez jamais task_canceled de votre code.Appelez plutôt cancel_current_task.

Lorsque les extrémités de travail annulé, la méthode concurrency::task::get lève concurrency::task_canceled. (En revanche, concurrency::task::wait retourne task_status::canceled et ne signale pas d'exception.) L'exemple suivant illustre ce comportement pour une suite de continuations de tâche. Une continuation de tâche est toujours appelée, même si l'antécédent est annulé.

// task-canceled.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t1 = create_task([]() -> int
    {
        // Cancel the task.
        cancel_current_task();
    });

    // Create a continuation that retrieves the value from the previous.
    auto t2 = t1.then([](task<int> t)
    {
        try
        {
            int n = t.get();
            wcout << L"The previous task returned " << n << L'.' << endl;
        }
        catch (const task_canceled& e)
        {
            wcout << L"The previous task was canceled." << endl;
        }
    });

    // Wait for all tasks to complete.
    t2.wait();
}
/* Output:
    The previous task was canceled.
*/

Les procédures suivantes VALUE- héritent de jeton de la tâche d'antécédent à moins qu'elles aient été créées avec un jeton significatif, les procédures suivantes passent à l'état immédiatement d'annulation même lorsque la tâche d'antécédent exécute toujours. Par conséquent, une exception signalée après l'annulation par la tâche antécédente n'est pas propagée aux tâches de continuation. L'annulation remplace toujours l'état de la tâche antécédente. L'exemple suivant ressemble au précédent, mais illustre le comportement d'une continuation de valeur.

auto t1 = create_task([]() -> int
{
    // Cancel the task.
    cancel_current_task();
});

// Create a continuation that retrieves the value from the previous.
auto t2 = t1.then([](int n)
{
    wcout << L"The previous task returned " << n << L'.' << endl;
});

try
{
    // Wait for all tasks to complete.
    t2.get();
}
catch (const task_canceled& e)
{
    wcout << L"The task was canceled." << endl;
}
/* Output:
    The task was canceled.
*/

Avertissement

Si vous ne transmettez pas un jeton d'annulation au constructeur task ou à la fonction concurrency::create_task, cette tâche n'est pas annulable.De plus, vous devez passer le même jeton d'annulation au constructeur de toutes les tâches imbriquées (autrement dit, les tâches créées dans le corps d'une autre tâche) pour annuler toutes les tâches simultanément.

Vous pourriez vouloir exécuter le code arbitraire lorsqu'un jeton d'annulation est annulé. Par exemple, si l'utilisateur choisit un bouton Annuler sur l'interface utilisateur pour annuler l'opération, vous pouvez désactiver ce bouton jusqu'à ce que l'utilisateur démarre une autre opération. L'exemple suivant montre comment utiliser la méthode concurrency::cancellation_token::register_callback pour stocker une fonction de rappel exécutée lorsqu'un jeton d'annulation est annulé.

// task-cancellation-callback.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    cancellation_token_source cts;
    auto token = cts.get_token();

    // An event that is set in the cancellation callback. 
    event e;

    cancellation_token_registration cookie;
    cookie = token.register_callback([&e, token, &cookie]()
    {
        wcout << L"In cancellation callback..." << endl;
        e.set();

        // Although not required, demonstrate how to unregister  
        // the callback.
        token.deregister_callback(cookie);
    });

    wcout << L"Creating task..." << endl;

    // Create a task that waits to be canceled.
    auto t = create_task([&e]
    {
        e.wait();
    }, token);

    // Cancel the task.
    wcout << L"Canceling task..." << endl;
    cts.cancel();

    // Wait for the task to cancel.
    t.wait();

    wcout << L"Done." << endl;
}
/* Sample output:
    Creating task...
    Canceling task...
    In cancellation callback...
    Done.
*/

Le document Parallélisme des tâches (runtime d'accès concurrentiel) explique la différence entre les continuations de valeur et de tâche. Si vous ne fournissez pas un objet cancellation_token à une tâche de continuation, la suite hérite du jeton d'annulation de la tâche antécédente de manières suivantes :

  • Une continuation de valeur hérite toujours du jeton d'annulation de la tâche antécédente.

  • Une continuation de tâche n'hérite jamais de jeton d'annulation de la tâche antécédente. La seule méthode pour fournir une continuation de tâche est d'annulée est de passer explicitement un jeton d'annulation.

Ces comportements ne sont pas affectés par une tâche censurée (autrement dit, une tâche qui signale une exception.) Dans ce cas, une continuation de valeur est annulée ; une continuation de tâche n'est pas annulée.

Avertissement

Une tâche créée dans une autre tâche (en d'autres termes, une tâche imbriquée) n'hérite pas du jeton d'annulation de la tâche parent.Seul un continuation basée sur la valeur hérite du jeton d'annulation de la tâche antécédente.

Conseil

Utilisez la méthode concurrency::cancellation_token::none lorsque vous appelez un constructeur ou une fonction qui prend un objet cancellation_token et vous ne voulez pas que l'opération soit annulable.

Vous pouvez également fournir un jeton d'annulation au constructeur d'un objet task_group ou structured_task_group. Un de ses aspects importants est que les groupes de tâches enfants héritent de ce jeton d'annulation. Pour obtenir un exemple qui illustre ce concept en utilisant la fonction concurrency::run_with_cancellation_token à exécuter pour appeler parallel_for, consultez Annuler les algorithmes parallèles plus loin dans ce document.

[Premières]

Jetons d'annulation et composition de la tâche

La section décrit les fonctions concurrency::when_all et concurrency::when_any, qui vous permet d'utiliser plusieurs tâches pour implémenter les modèles courants. Cette section explique comment ces fonctions fonctionnent avec des jetons d'annulation.

Lorsque vous spécifiez un jeton d'annulation soit à la fonction when_all soit à la fonction when_any, cette fonction annule uniquement quand ce jeton d'annulation est annulé ou lorsqu'un participant termine dans un état d'annulation ou lève une exception.

La fonction when_all hérite du jeton d'annulation de chaque tâche qui compose l'opération d'agrégation lorsque vous ne lui fournissez pas un jeton d'annulation. La tâche qui est retournée par when_all est annulée lorsque l'un de ces jetons est annulé et au moins une des tâches participantes qui n'a pas encore commencé ou exécute. Un tel comportement se produit lorsqu'une des tâches signale une exception - avec cette exception la tâche qui est retournée par when_all est immédiatement annulée.

Le runtime choisit le jeton d'annulation de la tâche qui est retournée par la fonction when_any quand cette tâche se termine. Si aucune des tâches participante ne se termine dans un état arrêté et qu'une ou plusieurs tâches signale une exception, l'une des tâches signalées est choisie pour terminer when_any et son jeton est choisie comme le jeton pour la dernière tâche. Si plus d'une tâche termine dans l'état terminé, la tâche qui est retournée par la tâche when_any dans un état terminé. Le runtime essaye de choisir une tâche terminée dont le jeton n'est pas annulé au moment de l'exécution afin que la tâche qui est retournée par when_any n'est pas immédiatement annulée même si d'autres tâches en cours d'exécution peuvent s'exécuter ultérieurement.

[Premières]

Utilisation de la méthode cancel pour annuler du travail parallèle

Les méthodes Concurrency::task_group::cancel et Concurrency::structured_task_group::cancel affectent à un groupe de tâches l'état annulé. Une fois que vous avez appelé cancel, le groupe de tâches ne démarre pas les tâches ultérieures. Les méthodes cancel peuvent être appelées par plusieurs tâches enfants. Une tâche annulée fait en sorte que les méthodes Concurrency::task_group::wait et Concurrency::structured_task_group::wait retournent l'état Concurrency::canceled.

Si un groupe de tâches est annulé, les appels de chaque tâche enfant dans le runtime peuvent déclencher un point d'interruption, ce qui fait en sorte que le runtime lève et intercepte un type d'exception interne pour annuler les tâches actives. Le runtime d'accès concurrentiel ne définit pas de points d'interruption spécifiques ; ils peuvent se produire dans n'importe quel appel au runtime. Le runtime doit gérer les exceptions qu'il lève afin d'effectuer l'annulation. Par conséquent, vous ne devez pas gérer d'exceptions inconnues dans le corps d'une tâche.

Si une tâche enfant exécute une opération qui prend du temps et ne fait pas appel au runtime, elle doit vérifier périodiquement l'annulation et s'arrêter de façon opportune. L'exemple suivant illustre une manière de déterminer quand le travail est annulé. La tâche t4 annule le groupe de tâches parent lorsqu'elle rencontre une erreur. La tâche t5 appelle parfois la méthode structured_task_group::is_canceling afin de vérifier l'annulation. Si le groupe de tâches parent est annulé, la tâche t5 imprime un message et quitte.

structured_task_group tg2;

// Create a child task.
auto t4 = make_task([&] {
   // Perform work in a loop. 
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work. 
      // If the work function fails, cancel the parent task 
      // and break from the loop. 
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }
});

// Create a child task.
auto t5 = make_task([&] {
   // Perform work in a loop. 
   for (int i = 0; i < 1000; ++i)
   {
      // To reduce overhead, occasionally check for  
      // cancelation. 
      if ((i%100) == 0)
      {
         if (tg2.is_canceling())
         {
            wcout << L"The task was canceled." << endl;
            break;
         }
      }

      // TODO: Perform work here.
   }
});

// Run the child tasks and wait for them to finish.
tg2.run(t4);
tg2.run(t5);
tg2.wait();

Cet exemple vérifie l'annulation chaque 100ème itération de la boucle de tâche. La fréquence à laquelle vous vérifiez l'annulation dépend de la quantité de travail effectuée par votre tâche et de la rapidité avec laquelle les tâches doivent répondre à l'annulation.

Si vous n'avez pas accès à l'objet groupe de tâches parent, appelez la fonction concurrency::is_current_task_group_canceling pour déterminer si le groupe de tâches parent est annulé.

La méthode cancel affecte seulement les tâches enfants. Par exemple, si vous annulez le groupe de tâches tg1 dans l'illustration de l'arborescence de travail parallèle, toutes les tâches dans l'arborescence (t1, t2, t3, t4 et t5) sont affectées. Si vous annulez le groupe de tâches imbriqué, tg2, seules les tâches t4 et t5 sont affectées.

Lorsque vous appelez la méthode cancel, tous les groupes de tâches enfants sont également annulés. Toutefois, l'annulation n'affecte aucun parent du groupe de tâches dans une arborescence de travail parallèle. Les exemples suivants, qui sont basés sur l'illustration de l'arborescence de travail parallèle, illustrent ce comportement.

Le premier de ces exemples crée une fonction de travail pour la tâche t4, qui est un enfant du groupe de tâches tg2. La fonction de travail appelle la fonction work en boucle. En cas d'échec d'un appel à work, la tâche annule son groupe de tâches parent. Cela provoque le basculement du groupe de tâches tg2 à l'état annulé, mais n'annule pas le groupe de tâches tg1.

auto t4 = make_task([&] {
   // Perform work in a loop. 
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work. 
      // If the work function fails, cancel the parent task 
      // and break from the loop. 
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }         
});

Ce deuxième exemple ressemble au premier, mais la tâche annule le groupe de tâches tg1. Cela affecte toutes les tâches dans l'arborescence (t1, t2, t3, t4 et t5).

auto t4 = make_task([&] {
   // Perform work in a loop. 
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work. 
      // If the work function fails, cancel all tasks in the tree. 
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg1.cancel();
         break;
      }
   }   
});

La classe structured_task_group n'est pas thread-safe. Par conséquent, une tâche enfant qui appelle une méthode de son objet structured_task_group parent produit un comportement non spécifié. Les exceptions à cette règle sont les méthodes structured_task_group::cancel et concurrency::structured_task_group::is_canceling. Une tâche fille peut appeler ces méthodes pour annuler le groupe de tâches parentes et vérifier l'annulation.

Avertissement

Bien que vous puissiez utiliser un jeton d'annuler pour annuler le travail soit effectué par un groupe de tâches qui s'exécute en tant qu'enfant de l'objet d'task, vous ne pouvez pas utiliser les méthodes d'task_group::cancel ou d'structured_task_group::cancel pour annuler les objets d'task qui s'exécutent à un groupe de tâches.

[Premières]

Utilisation d'exceptions pour annuler du travail parallèle

L'utilisation de l'annulation des tokens et de la méthode cancel sont plus efficaces que la gestion des exceptions pour annuler une arborescence de travail parallèle. Les jetons d'annulation et la méthode d'cancel annulent une tâche et toutes les tâches filles de façon hiérarchique. Inversement, la gestion des exceptions opère de bas en haut et doit annuler chaque groupe de tâches enfant indépendamment à mesure que l'exception se propage de bas en haut. La rubrique Gestion des exceptions dans le runtime d'accès concurrentiel explique comment le runtime d'accès concurrentiel utilise des exceptions pour signaler des erreurs. Toutefois, les exceptions n'indiquent pas toutes une erreur. Par exemple, un algorithme de recherche peut annuler son groupe de tâches associé lorsqu'il trouve le résultat. Toutefois, comme indiqué précédemment, la gestion des exceptions est moins efficace que l'utilisation de la méthode cancel pour annuler du travail parallèle.

Avertissement

Nous vous recommandons d'utiliser des exceptions pour annuler le travail parallèle seulement si nécessaire.Les jetons d'annulation et les méthodes de cancel de groupes de tâches sont une erreur plus efficace et inférieur plus encline.

Lorsque vous levez une exception dans le corps d'une fonction de travail que vous passez à un groupe de tâches, le runtime stocke cette exception et la marshale au contexte qui attend que le groupe de tâches se termine. Comme avec la méthode cancel, le runtime ignore les tâches qui n'ont pas encore commencé et n'accepte pas de nouvelles tâches.

Ce troisième exemple ressemble au deuxième, mais la tâche t4 lève une exception pour annuler le groupe de tâches tg2. Cet exemple utilise un bloc try-catch pour vérifier l'annulation lorsque le groupe de tâches tg2 attend la fin de l'exécution de ses tâches enfants. Comme le premier exemple, cela provoque le basculement du groupe de tâches tg2 à l'état annulé, mais n'annule pas le groupe de tâches tg1.

structured_task_group tg2;

// Create a child task.      
auto t4 = make_task([&] {
   // Perform work in a loop. 
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work. 
      // If the work function fails, throw an exception to  
      // cancel the parent task. 
      bool succeeded = work(i);
      if (!succeeded)
      {
         throw exception("The task failed");
      }
   }         
});

// Create a child task.
auto t5 = make_task([&] {
   // TODO: Perform work here.
});

// Run the child tasks.
tg2.run(t4);
tg2.run(t5);

// Wait for the tasks to finish. The runtime marshals any exception 
// that occurs to the call to wait. 
try
{
   tg2.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

Ce quatrième exemple utilise la gestion des exceptions pour annuler l'ensemble de l'arborescence de travail. L'exemple intercepte l'exception lorsque le groupe de tâches tg1 attend la fin de l'exécution de ses tâches enfants, plutôt que lorsque le groupe de tâches tg2 attend ses tâches enfants. Comme le deuxième exemple, cela provoque le basculement à l'état annulé des deux groupes de tâches dans l'arborescence, tg1 et tg2.

// Run the child tasks.
tg1.run(t1);
tg1.run(t2);
tg1.run(t3);   

// Wait for the tasks to finish. The runtime marshals any exception 
// that occurs to the call to wait. 
try
{
   tg1.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

Étant donné que les méthodes structured_task_group::wait et task_group::wait lèvent une exception lorsqu'une tâche enfant lève une exception, elles ne fournissent pas de valeur de retour.

[Premières]

Annulation d'algorithmes parallèles

Les algorithmes parallèles dans la bibliothèque PPL, par exemple parallel_for, reposent sur les groupes de tâches. Par conséquent, vous pouvez utiliser une grande partie des mêmes techniques pour annuler un algorithme parallèle.

Les exemples suivants illustrent plusieurs manières d'annuler un algorithme parallèle.

L'exemple suivant utilise la fonction d'run_with_cancellation_token pour appeler l'algorithme d'parallel_for. La fonction d'run_with_cancellation_token prend un jeton d'annulation comme argument et appelle la fonction de travail disponible en mode synchrone. Les algorithmes parallèles sont basés sur les tâches, ils héritent du jeton d'annulation de la tâche parent. Par conséquent, parallel_for peut répondre à l'annulation.

// cancel-parallel-for.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Call parallel_for in the context of a cancellation token.
    cancellation_token_source cts;
    run_with_cancellation_token([&cts]() 
    {
        // Print values to the console in parallel.
        parallel_for(0, 20, [&cts](int n)
        {
            // For demonstration, cancel the overall operation  
            // when n equals 11. 
            if (n == 11)
            {
                cts.cancel();
            }
            // Otherwise, print the value. 
            else
            {
                wstringstream ss;
                ss << n << endl;
                wcout << ss.str();
            }
        });
    }, cts.get_token());
}
/* Sample output:
    15
    16
    17
    10
    0
    18
    5
*/

L'exemple suivant utilise la méthode Concurrency::structured_task_group::run_and_wait pour appeler l'algorithme parallel_for. La méthode structured_task_group::run_and_wait attend que la tâche fournie se termine. L'objet structured_task_group permet à la fonction de travail d'annuler la tâche.

// To enable cancelation, call parallel_for in a task group.
structured_task_group tg;

task_group_status status = tg.run_and_wait([&] {
   parallel_for(0, 100, [&](int i) {
      // Cancel the task when i is 50. 
      if (i == 50)
      {
         tg.cancel();
      }
      else
      {
         // TODO: Perform work here.
      }
   });
});

// Print the task group status.
wcout << L"The task group status is: ";
switch (status)
{
case not_complete:
   wcout << L"not complete." << endl;
   break;
case completed:
   wcout << L"completed." << endl;
   break;
case canceled:
   wcout << L"canceled." << endl;
   break;
default:
   wcout << L"unknown." << endl;
   break;
}

Cet exemple génère la sortie suivante.

  

L'exemple suivant utilise la gestion des exceptions pour annuler une boucle parallel_for. Le runtime marshale l'exception au contexte appelant.

try
{
   parallel_for(0, 100, [&](int i) {
      // Throw an exception to cancel the task when i is 50. 
      if (i == 50)
      {
         throw i;
      }
      else
      {
         // TODO: Perform work here.
      }
   });
}
catch (int n)
{
   wcout << L"Caught " << n << endl;
}

Cet exemple génère la sortie suivante.

  

L'exemple suivant utilise un indicateur Boolean pour coordonner l'annulation dans une boucle parallel_for. Chaque tâche s'exécute car cet exemple n'utilise ni la méthode cancel ni la gestion des exceptions pour annuler le jeu entier de tâches. Par conséquent, cette technique peut imposer une charge de calcul supérieure à un mécanisme d'annulation.

// Create a Boolean flag to coordinate cancelation. 
bool canceled = false;

parallel_for(0, 100, [&](int i) {
   // For illustration, set the flag to cancel the task when i is 50. 
   if (i == 50)
   {
      canceled = true;
   }

   // Perform work if the task is not canceled. 
   if (!canceled)
   {
      // TODO: Perform work here.
   }
});

Chaque méthode d'annulation présente des avantages par rapport aux autres. Choisissez la méthode qui correspond leur mieux à vos besoins spécifiques.

[Premières]

Quand ne pas utiliser l'annulation

L'utilisation de l'annulation est appropriée lorsque chaque membre d'un groupe de tâches connexes peut quitter de façon opportune. Toutefois, il existe des scénarios où l'annulation peut ne pas convenir à votre application. Par exemple, l'annulation de tâche étant coopérative, le jeu entier de tâches n'est pas annulé si l'une des tâches est bloquée. Par exemple, si une tâche n'a pas encore commencé, mais qu'elle débloque une autre tâche active, elle ne démarre pas si le groupe de tâches est annulé. Cela peut provoquer un interblocage dans votre application. Voici un second exemple illustrant le fait que l'utilisation de l'annulation peut ne pas convenir : lorsqu'une tâche est annulée, mais que sa tâche enfant effectue une opération importante, telle que la libération d'une ressource. Étant donné que le jeu entier de tâches est annulé lorsque la tâche parente est annulée, cette opération ne s'exécutera pas. Pour obtenir un exemple illustrant ce point, consultez la section Comprendre comment l'annulation et la gestion des exceptions affectent la destruction d'objet des meilleures pratiques du document Bibliothèque de modèles parallèles.

[Premières]

Rubriques connexes

Titre

Description

Comment : utiliser l'annulation pour rompre une boucle parallèle

Indique comment utiliser l'annulation pour implémenter un algorithme de recherche parallèle.

Comment : utiliser la gestion des exceptions pour rompre une boucle parallèle

Indique comment utiliser la classe task_group pour écrire un algorithme de recherche pour une arborescence de base.

Gestion des exceptions dans le runtime d'accès concurrentiel

Décrit comment le runtime gère les exceptions levées par des groupes de tâches, des tâches légères et des agents asynchrones, et comment répondre aux exceptions dans vos applications.

Parallélisme des tâches (runtime d'accès concurrentiel)

Décrit les relations entre les tâches et les groupes de tâches, et comment utiliser des tâches structurées et non structurées dans vos applications.

Algorithmes parallèles

Décrit les algorithmes parallèles, qui exécutent de manière simultanée du travail sur des collections de données

Bibliothèque de modèles parallèles

Fournit une vue d'ensemble de la Bibliothèque de modèles parallèles.

Référence

task (Concurrency Runtime), classe

cancellation_token_source, classe

cancellation_token, classe

task_group, classe

structured_task_group, classe

parallel_for, fonction