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

Ce document décrit le rôle des tâches et des groupes de tâches pendant le runtime d'accès concurrentiel. Utilisez des groupes de tâches lorsque vous voulez exécuter simultanément au moins deux éléments de travail. Par exemple, supposez que vous avez un algorithme récursif qui divise le travail restant en deux partitions. Vous pouvez utiliser des groupes de tâches pour exécuter simultanément ces partitions. En revanche, utilisez des algorithmes parallèles, tel que Concurrency::parallel_for, lorsque vous souhaitez appliquer la même routine à chaque élément d'une collection en parallèle. Pour plus d'informations sur les algorithmes parallèles, consultez Algorithmes parallèles.

Tâches et groupes de tâches

Une tâche est une unité de travail qui exécute un travail spécifique. En général, une tâche peut s'exécuter en parallèle avec d'autres tâches et peut être décomposée en tâches supplémentaires, plus affinées. Un groupe de tâches organise une collection de tâches. Les groupes de tâches poussent les tâches vers une file d'attente de vol de travail. Le planificateur supprime les tâches de cette file d'attente et les exécute sur les ressources de calcul disponibles. Après avoir ajouté des tâches à un groupe de tâches, vous pouvez attendre que toutes les tâches soient terminées ou annuler les tâches qui n'ont pas encore commencé.

La bibliothèque PPL utilise les classes Concurrency::task_group et Concurrency::structured_task_group pour représenter des groupes de tâches, et la classe Concurrency::task_handle pour représenter des tâches. La classe task_handle encapsule le code qui exécute un travail. Ce code apparaît sous la forme d'une fonction lambda, d'un pointeur fonction ou objet de fonction. Il est souvent appelé fonction de travail. En général, vous n'avez pas besoin d'utiliser des objets task_handle directement. Au lieu de cela, vous passez des fonctions de travail à un groupe de tâches et le groupe de tâches crée et gère les objets task_handle.

La bibliothèque PPL divise les groupes de tâches en deux catégories : les groupes de tâches non structurés et les groupes de tâches structurés. La bibliothèque PPL utilise la classe task_group pour représenter des groupes de tâches non structurés et la classe structured_task_group pour représenter des groupes de tâches structurés.

Important

La bibliothèque PPL définit également l'algorithme Concurrency::parallel_invoke, qui utilise la classe structured_task_group pour exécuter un ensemble de tâches en parallèle. Étant donné que l'algorithme parallel_invoke a une syntaxe plus succincte, nous vous conseillons de l'utiliser au lieu d'utiliser la classe structured_task_group, lorsque vous le pouvez. La rubrique Algorithmes parallèles décrit parallel_invoke plus en détail.

Utilisez parallel_invoke lorsque vous voulez exécuter simultanément plusieurs tâches indépendantes et que vous devez attendre que toutes les tâches soient terminées avant de continuer. Utilisez task_group lorsque vous voulez exécuter simultanément plusieurs tâches indépendantes, mais que vous souhaitez attendre que les tâches se finissent ultérieurement. Par exemple, vous pouvez ajouter des tâches à un objet task_group et attendre qu'elles se terminent dans une autre fonction ou dans un autre thread.

Les groupes de tâches prennent en charge le concept d'annulation. L'annulation vous permet de signaler à toutes les tâches actives que vous souhaitez annuler l'opération globale. L'annulation empêche également le lancement des tâches qui n'ont pas encore commencé. Pour plus d'informations sur l'annulation, consultez Annulation dans la bibliothèque de modèles parallèles.

Le runtime fournit également un modèle de gestion des exceptions qui vous permet de lever une exception à partir d'une tâche et de gérer cette exception tout en attendant que le groupe de tâches associé se termine. Pour plus d'informations sur ce modèle de gestion des exceptions, consultez Gestion des exceptions dans le runtime d'accès concurrentiel.

Comparaison de task_group et de structured_task_group

Nous vous conseillons d'utiliser la classe task_group ou la classe parallel_invoke plutôt que la classe structured_task_group. Dans certains cas, vous pouvez cependant utiliser structured_task_group, par exemple lorsque vous écrivez un algorithme parallèle qui effectue un nombre variable de tâches ou qui nécessite la prise en charge de l'annulation. Cette section explique les différences entre les classes task_group et structured_task_group.

La classe task_group est thread-safe. Vous pouvez, par conséquent, ajouter des tâches à un objet task_group à partir de plusieurs threads et attendre ou annuler un objet task_group à partir de plusieurs threads. La construction et la destruction d'un objet structured_task_group doit se produire dans la même portée lexicale. De plus, toutes les opérations sur un objet structured_task_group doivent se produire sur le même thread. Les méthodes Concurrency::structured_task_group::cancel et Concurrency::structured_task_group::is_canceling font exception à cette règle. Une tâche enfant peut appeler ces méthodes pour annuler le groupe de tâches parent ou vérifier l'annulation à tout moment.

Vous pouvez effectuer des tâches supplémentaires sur un objet task_group après avoir appelé la méthode Concurrency::task_group::wait ou la méthode Concurrency::task_group::run_and_wait. En revanche, vous ne pouvez pas effectuer de tâches supplémentaires sur un objet structured_task_group après avoir appelé la méthode Concurrency::structured_task_group::wait ou la méthode Concurrency:: structured_task_group::run_and_wait.

Étant donné que la classe structured_task_group ne synchronise pas d'un thread à un autre, sa charge d'exécution est inférieure à celle de la classe task_group. Par conséquent, si votre problème ne nécessite pas la planification du travail à partir de plusieurs threads et que vous ne pouvez pas utiliser l'algorithme parallel_invoke, la classe structured_task_group, peut vous aider à écrire du code plus performant.

Si vous utilisez un objet structured_task_group à l'intérieur d'un autre objet structured_task_group, l'objet interne doit se terminer et être détruit avant que l'objet externe se termine. La classe task_group ne nécessite pas que les groupes de tâches imbriqués se terminent avant les groupes externes.

Les groupes de tâches non structurés et les groupes de tâches structurés utilisent les handles de tâches de différentes façons. Vous pouvez passer directement des fonctions de travail à un objet task_group. L'objet task_group pourra alors créer et gérer le handle de tâches pour vous. La classe structured_task_group nécessite la gestion d'un objet task_handle pour chaque tâche. Chaque objet task_handle doit rester valide pendant toute la durée de vie de l'objet structured_task_group associé. Utilisez la fonction Concurrency::make_task pour créer un objet task_handle, comme illustré dans l'exemple de base suivant :

// make-task-structure.cpp
// compile with: /EHsc
#include <ppl.h>

using namespace Concurrency;

int wmain()
{
   // Use the make_task function to define several tasks.
   auto task1 = make_task([] { /*TODO: Define the task body.*/ });
   auto task2 = make_task([] { /*TODO: Define the task body.*/ });
   auto task3 = make_task([] { /*TODO: Define the task body.*/ });

   // Create a structured task group and run the tasks concurrently.

   structured_task_group tasks;

   tasks.run(task1);
   tasks.run(task2);
   tasks.run_and_wait(task3);
}

Pour gérer les handles de tâche lorsque le nombre de tâches est variable, utilisez une routine d'allocation de tâches, comme _malloca ou une classe de conteneur, comme std::vector.

task_group et structured_task_group prennent tous les deux en charge l'annulation. Pour plus d'informations sur l'annulation, consultez Annulation dans la bibliothèque de modèles parallèles.

Exemple

L'exemple de base suivant montre comment utiliser des groupes de tâches. Cet exemple utilise l'algorithme parallel_invoke pour effectuer deux tâches simultanément. Chaque tâche ajoute une sous-tâche à un objet task_group. Notez que la classe task_group permet à plusieurs tâches d'y ajouter des tâches simultanément.

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

using namespace Concurrency;
using namespace std;

// Prints a message to the console.
template<typename T>
void print_message(T t)
{
   wstringstream ss;
   ss << L"Message from task: " << t << endl;
   wcout << ss.str(); 
}

int wmain()
{  
   // A task_group object that can be used from multiple threads.
   task_group tasks;

   // Concurrently add several tasks to the task_group object.
   parallel_invoke(
      [&] {
         // Add a few tasks to the task_group object.
         tasks.run([] { print_message(L"Hello"); });
         tasks.run([] { print_message(42); });
      },
      [&] {
         // Add one additional task to the task_group object.
         tasks.run([] { print_message(3.14); });
      }
   );

   // Wait for all tasks to finish.
   tasks.wait();
}

Voici un exemple de sortie pour cet exemple :

Message from task: Hello
Message from task: 3.14
Message from task: 42

Étant donné que l'algorithme parallel_invoke effectue des tâches simultanément, l'ordre des messages de sortie peut varier.

Pour obtenir des exemples complets utilisant l'algorithme parallel_invoke, consultez Comment : utiliser parallel_invoke pour écrire une routine de tri parallèle et Comment : utiliser parallel_invoke pour exécuter des opérations parallèles. Pour obtenir un exemple complet utilisant la classe task_group pour implémenter des tâches asynchrones futures, consultez Procédure pas à pas : implémentation de tâches futures.

Programmation fiable

Assurez-vous que vous comprenez le rôle de l'annulation et de la gestion des exceptions lorsque vous utilisez des groupes de tâches et des algorithmes parallèles. Par exemple, dans une arborescence de travail parallèle, une tâche qui est annulée empêche l'exécution des tâches enfants. Cela peut entraîner des problèmes si l'une des tâches enfants effectue une opération importante pour votre application, telle que la libération d'une ressource. En outre, si une tâche enfant lève une exception, cette exception peut se propager à travers un destructeur d'objet et provoquer un comportement non défini dans votre application. Pour obtenir un exemple illustrant ces éléments, consultez la section Comprendre comment l'annulation et la gestion des exceptions affectent la destruction d'objet des meilleures pratiques du document relatif à la bibliothèque de modèles parallèles (PPL). Pour plus d'informations sur les modèles d'annulation et de gestion des exceptions dans la bibliothèque PPL, consultez Annulation dans la bibliothèque de modèles parallèles et Gestion des exceptions dans le runtime d'accès concurrentiel.

Rubriques connexes

Référence

task_group, classe

parallel_invoke, fonction

structured_task_group, classe

Historique des modifications

Date

Historique

Motif

Mars 2011

Ajout d'informations sur le rôle de l'annulation et de la gestion des exceptions lorsque vous utilisez des groupes de tâches et des algorithmes parallèles.

Améliorations apportées aux informations.

Juillet 2010

Réorganisation du contenu.

Améliorations apportées aux informations.

Mai 2010

Développement des instructions.

Améliorations apportées aux informations.