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), comment annuler un travail parallèle et comment déterminer quand le travail parallèle est annulé.
Remarque
Le runtime utilise la gestion des exceptions pour implémenter l'annulation. N'interceptez pas et ne gérez pas ces exceptions dans votre code. De plus, nous vous recommandons d'écrire du code protégé contre les exceptions dans les corps de fonction pour vos tâches. Par exemple, vous pouvez utiliser le modèle d’initialisation d’acquisition de ressources (RAII) pour vous assurer que les ressources sont correctement gérées lorsqu’une exception est levée dans le corps d’une tâche. Pour obtenir un exemple complet qui utilise le modèle RAII pour nettoyer une ressource dans une tâche annulable, consultez procédure pas à pas : Suppression du travail d’un thread d’interface utilisateur.
Points clés
L’annulation est coopérative et implique une 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 un 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 lancer l’annulation et la fonction concurrency ::cancel_current_task pour répondre à l’annulation. Utilisez la méthode concurrency ::cancellation_token ::is_canceled pour vérifier si une autre tâche a demandé l’annulation.
L'annulation ne se produit pas immédiatement. Bien que le nouveau travail ne soit pas démarré si une tâche ou un groupe de tâches est annulé, le travail actif doit rechercher et répondre à l’annulation.
Une continuation basée sur des valeurs hérite du jeton d'annulation de sa tâche antécédente. Une continuation basée sur des tâches n‘hérite jamais du jeton de sa tâche antécédente.
Utilisez la méthode concurrency ::cancellation_token ::none lorsque vous appelez un constructeur ou une fonction qui prend un
cancellation_token
objet, mais que vous ne souhaitez pas que l’opération soit annulable. En outre, si vous ne transmettez pas de jeton d’annulation au constructeur concurrency ::task ou à la fonction concurrency ::create_task , cette tâche n’est pas annulable.
Dans ce document
Arborescences de travail parallèles
La bibliothèque de modèles parallèles (PPL) utilise des tâches et des groupes de tâches pour gérer les tâches et les calculs affinés. 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 les groupes de tâches effectuent.
L’exemple suivant montre le code requis 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 arborescence de travail similaire. La classe concurrency ::task prend également en charge la notion d’arborescence de travail. Toutefois, une arborescence task
est une arborescence des dépendances. Dans une arborescence task
, le travail futur s'accomplit après le travail en cours. Dans une arborescence de groupes de tâches, le travail interne s'accomplit avant le travail externe. Pour plus d’informations sur les différences entre les tâches et les groupes de tâches, consultez Parallélisme des tâches.
[Haut]
Annulation de tâches parallèles
Il existe plusieurs façons d'annuler un travail parallèle. Le meilleur moyen consiste à utiliser un jeton d'annulation. Les groupes de tâches prennent également en charge la méthode concurrency ::task_group ::cancel et la méthode concurrency ::structured_task_group ::cancel . La dernière façon consiste à lever une exception dans le corps d’une fonction de travail de tâche. Quelle que soit la méthode choisie, comprenez bien que l'annulation ne se produit pas immédiatement. Bien que le nouveau travail ne soit pas démarré si une tâche ou un groupe de tâches est annulé, le travail actif doit rechercher et répondre à l’annulation.
Pour obtenir d’autres 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, utilisation de l’annulation pour arrêter à partir d’une boucle parallèle et procédure : utiliser la gestion des exceptions pour arrêter à partir d’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 travail, le runtime ne démarre pas le nouveau processus qui souscrit à ce jeton. Le travail déjà actif peut utiliser la fonction membre is_canceled pour surveiller le jeton d’annulation et arrêter quand il le peut.
Pour lancer l’annulation, appelez la méthode concurrency ::cancellation_token_source ::cancel . Vous répondez à l'annulation des façons suivantes :
Pour
task
les objets, utilisez la fonction concurrency ::cancel_current_task .cancel_current_task
annule la tâche en cours et toutes ses continuations basées sur des valeurs. (Il n’annule pas le jeton d’annulation associé à la tâche ou à ses continuations.)Pour les groupes de tâches et les algorithmes parallèles, utilisez la fonction concurrency ::is_current_task_group_canceling pour détecter l’annulation et le retour dès que possible à partir du corps de la tâche lorsque cette fonction retourne
true
. (N’appelez pascancel_current_task
à partir d’un groupe de tâches.)
L'exemple suivant montre le premier modèle de base pour l'annulation de tâches. Le corps de la tâche vérifie parfois l'annulation dans une boucle.
// task-basic-cancellation.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <concrt.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 (token.is_canceled())
{
// 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
se lance ; par conséquent, il est inutile de retourner explicitement un résultat à partir de la boucle ou fonction actuelle.
Conseil
Vous pouvez également appeler la fonction concurrency ::interruption_point au lieu de cancel_current_task
.
Il est important d’appeler cancel_current_task
quand vous répondez à l’annulation, car cela fait passer la tâche à l’état annulé. Si vous retournez un résultat trop tôt au lieu d'appeler cancel_current_task
, l'opération passe à l'état terminé et toutes les continuations basées sur des valeurs sont exécutées.
Attention
Ne levez jamais task_canceled
à partir de votre code. Appelez cancel_current_task
à la place.
Lorsqu’une tâche se termine à l’état annulé, la méthode concurrency ::get lève concurrency ::task_canceled. (À l’inverse, concurrency ::task ::wait retourne task_status ::canceled et ne lève pas.) L’exemple suivant illustre ce comportement pour une continuation basée sur des tâches. Une continuation basée sur des tâches est toujours appelée, même quand la tâche antécédente est annulée.
// 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.
*/
Étant donné que les continuations basées sur des valeurs héritent du jeton de leur tâche antécédente, sauf si elles ont été créées avec un jeton explicite, les continuations entrent immédiatement dans l’état annulé même quand la tâche antécédente est encore en cours d’exécution. Par conséquent, toute exception levée par la tâche antécédente, après l’annulation n’est pas propagée vers les 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 il illustre le comportement d'une continuation basée sur des valeurs.
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.
*/
Attention
Si vous ne transmettez pas de jeton d’annulation au constructeur ou à la task
fonction concurrency ::create_task , cette tâche n’est pas annulable. En outre, vous devez passer le même jeton d’annulation au constructeur de toutes les tâches imbriquées (autrement dit, les tâches qui sont créées dans le corps d’une autre tâche) pour annuler toutes les tâches simultanément.
Vous voulez peut-être exécuter du code arbitraire quand un jeton d'annulation est annulé. Par exemple, si votre 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 inscrire une fonction de rappel qui s’exécute 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 parallélisme des tâches du document explique la différence entre les continuations basées sur des valeurs et celles basées sur des tâches. Si vous ne fournissez pas un objet cancellation_token
à une tâche de continuation, la continuation hérite du jeton d’annulation de la tâche antécédente comme suit :
Une continuation basée sur des valeurs hérite toujours du jeton d‘annulation de la tâche antécédente.
Une continuation basée sur des tâches n'hérite jamais du jeton d'annulation de la tâche antécédente. La seule façon de rendre une continuation basée sur des tâches annulable consiste à passer explicitement un jeton d'annulation.
Ces comportements ne sont pas affectés par une tâche qui a échoué (autrement dit, une tâche qui lève une exception). Dans ce cas, une continuation basée sur des valeurs est annulée ; Une continuation basée sur des tâches n’est pas annulée.
Attention
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 parente. Seule une continuation basée sur des valeurs hérite du jeton d'annulation de sa 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 cancellation_token
objet et que vous ne souhaitez 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
. Autre aspect important : les groupes de tâches enfants héritent de ce jeton d'annulation. Pour obtenir un exemple qui illustre ce concept à l’aide de la fonction concurrency ::run_with_cancellation_token à exécuter pour appeler parallel_for
, consultez Annuler des algorithmes parallèles plus loin dans ce document.
[Haut]
Jetons d'annulation et composition de la tâche
Les fonctions concurrency ::when_all et concurrency ::when_any peuvent vous aider à composer plusieurs tâches pour implémenter des modèles courants. Cette section décrit la manière dont ces fonctions utilisent des jetons d'annulation.
Lorsque vous fournissez un jeton d’annulation à la fonction et when_any
à la when_all
fonction, cette fonction annule uniquement lorsque ce jeton d’annulation est annulé ou quand l’une des tâches du participant se termine dans un état annulé ou lève une exception.
La fonction when_all
hérite du jeton d’annulation de chaque tâche qui compose l’opération globale quand vous ne lui fournissez pas de jeton d’annulation. La tâche retournée when_all
est annulée lorsque l’un de ces jetons est annulé et qu’au moins une des tâches du participant n’a pas encore démarré ou est en cours d’exécution. Un comportement similaire se produit lorsqu’une des tâches lève une exception : la tâche retournée when_all
est immédiatement annulée avec cette exception.
Le runtime sélectionne le jeton d’annulation de la tâche retournée de la fonction when_any
quand cette tâche se termine. Si aucune des tâches participantes ne se termine dans un état terminé et qu’une ou plusieurs tâches lèvent une exception, une des tâches déclenchées est choisie pour effectuer l’opération when_any
et son jeton est choisi en tant que jeton pour la tâche finale. Si plusieurs tâches se terminent dans l’état terminé, la tâche retournée de la tâche when_any
se termine dans un état terminé. Le runtime essaie de sélectionner une tâche terminée dont le jeton n’est pas annulé au moment de l’exécution pour que la tâche retournée de when_any
ne soit pas immédiatement annulée même si d’autres tâches en cours d’exécution risquent de se terminer plus tard.
[Haut]
Utilisation de la méthode cancel pour annuler le travail parallèle
Les méthodes concurrency ::task_group ::cancel et concurrency ::structured_task_group ::cancel définissent un groupe de tâches sur l’état annulé. Après avoir appelé cancel
, le groupe de tâches ne démarre pas les tâches futures. Les méthodes cancel
peuvent être appelées par plusieurs tâches enfants. Une tâche annulée entraîne le retour des méthodes concurrency ::task_group ::wait et concurrency ::structured_task_group ::wait pour renvoyer 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 entraîne la levée et l’interception d’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 tout appel au runtime. Le runtime doit gérer les exceptions qu'il lève pour effectuer l'annulation. Par conséquent, ne gérez pas les exceptions inconnues dans le corps d'une tâche.
Si une tâche enfant effectue une opération chronophage et n'appelle pas dans le runtime, elle doit régulièrement vérifier l'annulation et se fermer en temps voulu. L'exemple suivant montre une façon de déterminer le moment auquel le travail est annulé. La tâche t4
annule le groupe de tâches parent quand elle rencontre une erreur. La tâche t5
appelle de temps en temps la méthode structured_task_group::is_canceling
pour vérifier l'annulation. Si le groupe de tâches parent est annulé, la tâche t5
imprime un message et se ferme.
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 sur chaque 100e 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 vitesse à laquelle vous avez besoin que les tâches répondent à l'annulation.
Si vous n’avez pas accès à l’objet de 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 uniquement 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 de 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.
Quand vous appelez la méthode cancel
, tous les groupes de tâches enfants sont également annulés. Toutefois, l'annulation n'affecte pas les parents du groupe de tâches dans une arborescence de travail parallèle. Les exemples suivants illustrent ce point en s'appuyant sur l'illustration de l'arborescence de travail parallèle.
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
dans une boucle. Si un appel à work
échoue, la tâche annule son groupe de tâches parent. Cette annulation entraîne l'entrée du groupe de tâches tg2
dans l'état annulé, mais elle 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, sauf que la tâche annule le groupe de tâches tg1
. Cette annulation affecte toutes les tâches de 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 entraîne un comportement non spécifié. Les exceptions à cette règle sont les structured_task_group::cancel
méthodes concurrency ::structured_task_group ::is_canceling . Une tâche enfant peut appeler ces méthodes pour annuler le groupe de tâches parent et vérifier l'annulation.
Attention
Bien que vous puissiez utiliser un jeton d’annulation pour annuler un travail effectué par un groupe de tâches qui s’exécute en tant qu’enfant d’un objet task
, vous ne pouvez pas utiliser les méthodes task_group::cancel
ou structured_task_group::cancel
pour annuler des objets task
qui s’exécutent dans un groupe de tâches.
[Haut]
Utilisation d’exceptions pour annuler le travail parallèle
L’utilisation de jetons d’annulation et 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 cancel
annulent une tâche et toutes les tâches enfants de haut en bas. À l’inverse, la gestion des exceptions fonctionne de bas en haut et doit annuler chaque groupe de tâches enfant indépendamment puisque l’exception se propage vers le haut. La rubrique Gestion des exceptions explique comment le runtime d’accès concurrentiel utilise des exceptions pour communiquer des erreurs. Toutefois, toutes les exceptions n'indiquent pas une erreur. Par exemple, un algorithme de recherche peut annuler sa tâche associée quand il trouve le résultat. Toutefois, comme mentionné précédemment, la gestion des exceptions est moins efficace que l'utilisation de la méthode cancel
pour annuler un travail parallèle.
Attention
Nous vous recommandons d'utiliser des exceptions pour annuler un travail parallèle uniquement quand cela est nécessaire. Les jetons d'annulation et les méthodes cancel
de groupe de tâches sont plus efficaces et moins sujets à erreur.
Quand 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 vers le 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 démarré et qui n’acceptent pas de nouvelles tâches.
Ce troisième exemple ressemble au deuxième, sauf que la tâche t4
lève une exception pour annuler le groupe de tâches tg2
. Cet exemple utilise un try
-catch
bloc pour vérifier l’annulation lorsque le groupe tg2
de tâches attend que ses tâches enfants se terminent. Comme le premier exemple, cela entraîne l'entrée du groupe de tâches tg2
dans l'état annulé, mais cela 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 toute l’arborescence de travail. L’exemple intercepte l’exception quand le groupe de tâches tg1
attend que ses tâches enfants se terminent plutôt que quand le groupe de tâches tg2
attend ses tâches enfants. Comme le deuxième exemple, cela entraîne l'entrée des deux groupes de tâches de l'arborescence, tg1
et tg2
, dans l'état annulé.
// 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 task_group::wait
et structured_task_group::wait
se déclenchent quand une tâche enfant lève une exception, vous ne recevez pas de valeur de retour de leur part.
[Haut]
Annulation d’algorithmes parallèles
Les algorithmes parallèles de la bibliothèque de modèles parallèles (PPL), par exemple, parallel_for
, s’appuient sur des groupes de tâches. Par conséquent, vous pouvez utiliser nombre 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 run_with_cancellation_token
pour appeler l'algorithme parallel_for
. La fonction run_with_cancellation_token
prend un jeton d'annulation en tant qu'argument et appelle la fonction de travail fournie de manière synchrone. Étant donné que les algorithmes parallèles s'appuient sur des 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 produit 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 vers le contexte d’appel.
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 produit la sortie suivante.
Caught 50
L'exemple suivant utilise un indicateur booléen pour coordonner l'annulation dans une boucle parallel_for
. Chaque tâche s'exécute car cet exemple n'utilise pas la méthode cancel
ou la gestion des exceptions pour annuler l'ensemble des tâches. Par conséquent, cette technique peut imposer une charge de calcul plus importante qu'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 répond à vos besoins spécifiques.
[Haut]
Quand ne pas utiliser l’annulation
L'utilisation de l'annulation est appropriée quand chaque membre d'un groupe de tâches connexes peut quitter le groupe en temps voulu. Toutefois, il existe certains scénarios où l'annulation peut ne pas convenir à votre application. Par exemple, étant donné que l’annulation de tâches est coopérative, l’ensemble des tâches n’est pas annulé si une tâches individuelle est bloquée. Par exemple, si une seule tâche n’a pas encore démarré, mais qu’elle débloque une autre tâche active, elle ne démarre pas si le groupe de tâches est annulé. Il existe alors un risque d'interblocage dans votre application. Deuxième exemple d’utilisation inappropriée de l’annulation : quand une tâche est annulée, alors que sa tâche enfant effectue une opération importante, comme la libération d’une ressource. Étant donné que l’ensemble des tâches est annulé quand la tâche parente est annulée, cette opération ne s’exécute pas. Pour obtenir un exemple illustrant ce point, consultez la section Comprendre comment la gestion des annulations et des exceptions affecte la destruction d’objets dans la rubrique Meilleures pratiques de la bibliothèque de modèles parallèles.
[Haut]
Rubriques connexes
Intitulé | Description |
---|---|
Guide pratique pour utiliser l’annulation pour rompre une boucle parallèle | Montre comment utiliser l'annulation pour implémenter un algorithme de recherche parallèle. |
Guide pratique pour utiliser la gestion des exceptions pour rompre une boucle parallèle | Montre comment utiliser la classe task_group pour écrire un algorithme de recherche pour une arborescence de base. |
Gestion des exceptions | 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, ainsi que comment répondre aux exceptions dans vos applications. |
Parallélisme des tâches | Décrit les relations entre les tâches et les groupes de tâches, ainsi que comment vous pouvez 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 simultanément 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