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

Cette rubrique 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 un groupe de tâches est annulé.

Sections

  • Arborescences de travail parallèle

  • Annulation de tâches parallèles

  • Annulation d'algorithmes parallèles

  • Quand ne pas utiliser l'annulation

Arborescences de travail parallèle

La bibliothèque PPL utilise 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 des tâches.

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();   
}

[retour en haut]

Annulation de tâches parallèles

Il y a deux façons d'annuler du travail parallèle. L'une consiste à appeler la méthode Concurrency::task_group::cancel ou la méthode Concurrency::structured_task_group::cancel. L'autre consiste à lever une exception dans le corps d'une fonction de travail de tâche.

La méthode cancel est plus efficace que la gestion des exceptions pour annuler une arborescence de travail parallèle. La méthode cancel annule un groupe de tâches et tous les groupes de tâches enfants de haut en bas. 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.

Les sections suivantes montrent comment utiliser la méthode cancel et la gestion des exceptions pour annuler du travail parallèle. Pour obtenir plus d'exemples d'annulation de tâches parallèles, consultez Comment : utiliser l'annulation pour rompre une boucle parallèle et Comment : utiliser la gestion des exceptions pour rompre une boucle parallèle.

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é.

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.

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.

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 enfant peut appeler ces méthodes pour annuler la tâche parente et vérifier l'annulation.

Utilisation d'exceptions pour annuler du travail parallèle

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.

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.

Déterminer quand le travail est annulé

L'annulation est coopérative. Par conséquent, elle ne se produit pas immédiatement. 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é.

[retour en haut]

Annulation d'algorithmes parallèles

Les algorithmes parallèles dans la bibliothèque PPL, par exemple Concurrency::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 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 :

The task group status is: canceled.

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 :

Caught 50

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.

[retour en haut]

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 cet élément, consultez la section Comprendre comment l'annulation et la gestion des exceptions affectent la destruction d'objet des meilleures pratiques de la rubrique relative aux bibliothèques de modèles parallèles (PPL).

[retour en haut]

Rubriques connexes

Référence

task_group, classe

structured_task_group, classe

parallel_for, fonction

Historique des modifications

Date

Historique

Motif

Mars 2011

Ajout d'un autre cas à la section Quand ne pas utiliser l'annulation.

Améliorations apportées aux informations.