Partager via


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

Le runtime d'accès concurrentiel utilise la gestion des exceptions C++ pour communiquer de nombreux genres d'erreurs. Ces erreurs incluent l'utilisation non valide du runtime, des erreurs d'exécution telles que l'échec d'acquisition d'une ressource et les erreurs qui se produisent dans les fonctions de travail que vous fournissez aux tâches et aux groupes de tâches. Lorsqu'une tâche ou un groupe de tâches lève une exception, le runtime conserve cette exception et la marshale au contexte qui attend que la tâche ou le groupe de tâches se termine. Pour des composants tels que les tâches légères et les agents, le runtime ne gère pas les exceptions pour vous. Dans ces cas-là, vous devez implémenter votre propre mécanisme de gestion des exceptions. Cette rubrique décrit comment le runtime gère les exceptions levées par tâches, des groupes de tâches, des tâches légères et des agents asynchrones, et comment répondre aux exceptions dans vos applications.

Points clés

  • Lorsqu'une tâche ou un groupe de tâches lève une exception, le runtime conserve cette exception et la marshale au contexte qui attend que la tâche ou le groupe de tâches se termine.

  • Lorsque c'est possible, entourer chaque appel à concurrency::task::get et concurrency::task::wait dans un bloc try/catch pour manipuler les erreurs que vous pouvez récupérer. Le runtime termine l'application si une tâche lève une exception et que cette exception n'est pas interceptée par la tâche, une de ses procédures suivantes ou l'application principale.

  • Un prolongement d'une tâche compile toujours; que l'antécédent soit terminé correctement, qu'une exception soit levée, ou ait été annulée n'importe pas. Un prolongement basé sur la valeur ne fonctionne pas si la tâche antécédente lève ou annule.

  • Étant donné que le prolongement des tâches fonctionne toujours, envisagez s'il faut ajouter un prolongement des tâches par à la fin de la chaîne de prolongement. Cela peut permettre de vous assurer que votre code considère toutes les exceptions.

  • Le runtime lève concurrency::task_canceled lorsque vous appelez concurrency::task::get et cette tâche est annulée.

  • Le runtime ne gère pas d'exceptions pour des tâches légères et des agents.

Dans ce document

  • Tâches et continuations

  • Groupes de tâches et algorithmes parallèles

  • Exceptions levées par le runtime

  • Exceptions multiples

  • Annulation

  • Tâches légères

  • Agents asynchrones

Tâches et continuations

Cette section décrit comment le runtime gère les exceptions qui sont levées par les objets concurrency::task et leur prolongement. Pour plus d'informations sur la tâche et le modèle de prolongement, consultez Parallélisme des tâches (runtime d'accès concurrentiel).

Lorsque vous levez une exception dans le corps d'une fonction de travail que vous passez à un object task, le runtime le stocke et la marshale au contexte qui appelle concurrency::task::get ou concurrency::task::wait. Le document Parallélisme des tâches (runtime d'accès concurrentiel) décrit les prolongements basés sur la valeur versus ceux basés sur la tâche , mais pour résumer, un prolongement basé sur la valeur prend un paramètre de type T et prolongements basés sur la tâche prend un paramètre de type task<T>. Si une tâche qui lève une exception a un ou plusieurs prolongements basés sur la valeur, ces prolongements ne sont pas planifiées pour s'exécuter. L'exemple suivant illustre ce comportement :

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

using namespace concurrency;
using namespace std;

int wmain()
{
    wcout << L"Running a task..." << endl;
    // Create a task that throws.
    auto t = create_task([]
    {
        throw exception();
    });

    // Create a continuation that prints its input value.
    auto continuation = t.then([]
    {
        // We do not expect this task to run because 
        // the antecedent task threw.
        wcout << L"In continuation task..." << endl;
    });

    // Wait for the continuation to finish and handle any  
    // error that occurs. 
    try
    {
        wcout << L"Waiting for tasks to finish..." << endl;
        continuation.wait();

        // Alternatively, call get() to produce the same result. 
        //continuation.get();
    }
    catch (const exception& e)
    {
        wcout << L"Caught exception." << endl;
    }
}
/* Output:
    Running a task...
    Waiting for tasks to finish...
    Caught exception.
*/

Un prolongement de tâches qui permet de gérer n'importe quelle exception qui est levée par la tâche antécédente. Un prolongement d'une tâche compile toujours; que la tâche antécédente soit terminée correctement, qu'une exception soit levée, ou ait été annulée n'importe pas. Lorsqu'une tâche lève une exception, ses prolongements basés sur la tâche sont planifiés pour s'exécuter. L'exemple suivant montre une tâche qui lève toujours une exception. La tâche a deux prolongement; un est basé sur la valeur et l'autre est basé sur la tâche. Les exceptions basées sur les tâches sont toujours exécutées, et par conséquent peuvent intercepter l'exception levée par la tâche d'antécédent. Lorsque l'exemple attend les deux prolongements pour terminer, l'exception est générée de nouveau car l'exception de la tâche est toujours levée lorsque task::get ou task::wait est appelé.

// eh-continuations.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{    
    wcout << L"Running a task..." << endl;
    // Create a task that throws.
    auto t = create_task([]() -> int
    {
        throw exception();
        return 42;
    });

    // 
    // Attach two continuations to the task. The first continuation is   
    // value-based; the second is task-based. 

    // Value-based continuation.
    auto c1 = t.then([](int n)
    {
        // We don't expect to get here because the antecedent  
        // task always throws.
        wcout << L"Received " << n << L'.' << endl;
    });

    // Task-based continuation.
    auto c2 = t.then([](task<int> previousTask)
    {
        // We do expect to get here because task-based continuations 
        // are scheduled even when the antecedent task throws. 
        try
        {
            wcout << L"Received " << previousTask.get() << L'.' << endl;
        }
        catch (const exception& e)
        {
            wcout << L"Caught exception from previous task." << endl;
        }
    });

    // Wait for the continuations to finish. 
    try
    {
        wcout << L"Waiting for tasks to finish..." << endl;
        (c1 && c2).wait();
    }
    catch (const exception& e)
    {
        wcout << L"Caught exception while waiting for all tasks to finish." << endl;
    }
}
/* Output:
    Running a task...
    Waiting for tasks to finish...
    Caught exception from previous task.
    Caught exception while waiting for all tasks to finish.
*/

Nous vous recommandons les procédures suivantes des tâches pour intercepter les exceptions que vous pouvez gérer. Étant donné que le prolongement des tâches fonctionne toujours, envisagez s'il faut ajouter un prolongement des tâches à la fin de la chaîne de prolongement. Cela peut permettre de vous assurer que votre code considère toutes les exceptions. L'exemple suivant illustre une chaîne simple de prolongation basée sur la valeur. La troisième tâche dans les lancés de chaine, et par conséquent n'importe quel prolongement basé sur la valeur qui la suivent ne sont pas exécutés. Toutefois, le dernier prolongement est basé sur des tâches, et par conséquent s'exécute toujours. Le dernier prolongement gère l'exception lancée par la troisième tâche.

Nous recommandons que vous interceptiez des exceptions aussi spécifiques que possible. Vous pouvez omettre le prolongement basé sur les tâches si vous n'avez pas d'exceptions spécifiques à intercepter. Toute exception restera non prise en charge et peut mettre fin à l'application.

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

using namespace concurrency;
using namespace std;

int wmain()
{
    int n = 1;
    create_task([n]
    {
        wcout << L"In first task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](int n)
    {
        wcout << L"In second task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](int n)
    {
        wcout << L"In third task. n = ";
        wcout << n << endl;

        // This task throws. 
        throw exception();
        // Not reached. 
        return n * 2;

    }).then([](int n)
    {
        // This continuation is not run because the previous task throws.
        wcout << L"In fourth task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](task<int> previousTask)
    {
        // This continuation is run because it is value-based. 
        try
        {
            // The call to task::get rethrows the exception.
            wcout << L"In final task. result = ";
            wcout << previousTask.get() << endl;
        }
        catch (const exception&)
        {
            wcout << L"<exception>" << endl;
        }
    }).wait();
}
/* Output:
    In first task. n = 1
    In second task. n = 2
    In third task. n = 4
    In final task. result = <exception>
*/

Conseil

Vous pouvez utiliser la méthode concurrency::task_completion_event::set_exception pour associer une exception avec un événement d'exécution de la tâche.Le document Parallélisme des tâches (runtime d'accès concurrentiel) décrit plus en détail la classe concurrency::task_completion_event .

concurrency::task_canceled est un type d'exception runtime important qui sont liées à task. Le runtime enlève task_canceled lorsque vous appelez task::get et cette tâche est annulée. (En revanche, task::wait retourne task_status::canceled et ne lève pas une exception.) Vous pouvez repérer et gérer cette exception d'un prolongement basé sur les tâches ou lorsque vous appelez task::get. Pour plus d'informations sur l'annulation des tâches , consultez Annulation dans la bibliothèque de modèles parallèles.

Avertissement

N'enlevez jamais task_canceled de votre code.Appel concurrency::cancel_current_task à la place.

Le runtime termine l'application si une tâche lève une exception et que cette exception n'est pas interceptée par la tâche, une de ses procédures suivantes ou l'application principale. Si votre application se bloque, vous pouvez configurer Visual Studio pour arrêter lorsque des exceptions C++ sont levées. Après avoir diagnostiqué l'emplacement de l'exception non gérée, utilisez un prolongement basé sur les tâches pour la gérer.

La section Exceptions Levées par le runtime dans ce document explique comment travailler avec des exceptions d'exécution plus en détail.

[Premières]

Groupes de tâches et algorithmes parallèles

Cette section décrit comment le runtime gère les exceptions levées par des groupes de tâches. Cette section s'applique également aux algorithmes parallèles tels que concurrency::parallel_for, car ces algorithmes reposent sur les groupes de tâches.

Avertissement

Assurez-vous de comprendre les effets des exceptions sur les tâches dépendantes.Pour les méthodes recommandées sur la gestion des exceptions des tâches ou des algorithmes parallèles, consultez la section " Comprendre l'impact de l'annulation et de la gestion des exceptions sur la destruction des objets section des meilleures pratiques dans la rubrique de bibliothèque de modèles parallèles.

Pour plus d'informations sur les groupes de tâches, consultez Parallélisme des tâches (runtime d'accès concurrentiel). Pour plus d'informations sur les algorithmes parallèles, consultez Algorithmes parallèles.

Lorsque vous levez une exception dans le corps d'une fonction de travail que vous passez à un objet concurrency::task_group ou concurrency::structured_task_group, le runtime stocke cette exception et la marshale au contexte qui appelle concurrency::task_group::wait, concurrency::structured_task_group::wait, concurrency::task_group::run_and_wait ou concurrency::structured_task_group::run_and_wait. Le runtime arrête également toutes les tâches actives qui sont dans le groupe de tâches (y compris celles des groupes de tâches enfants) et ignore toutes les tâches qui n'ont pas encore démarré.

L'exemple suivant montre la structure de base d'une fonction de travail qui lève une exception. L'exemple utilise un objet task_group pour imprimer les valeurs de deux objets point en parallèle. La fonction de travail print_point imprime les valeurs d'un objet point sur la console. La fonction de travail lève une exception si la valeur d'entrée est NULL. Le runtime stocke cette exception et la marshale au contexte qui appelle task_group::wait.

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

using namespace concurrency;
using namespace std;

// Defines a basic point with X and Y coordinates. 
struct point
{
   int X;
   int Y;
};

// Prints the provided point object to the console. 
void print_point(point* pt)
{
   // Throw an exception if the value is NULL. 
   if (pt == NULL)
   {
      throw exception("point is NULL.");
   }

   // Otherwise, print the values of the point.
   wstringstream ss;
   ss << L"X = " << pt->X << L", Y = " << pt->Y << endl;
   wcout << ss.str();
}

int wmain()
{
   // Create a few point objects.
   point pt = {15, 30};
   point* pt1 = &pt;
   point* pt2 = NULL;

   // Use a task group to print the values of the points.
   task_group tasks;

   tasks.run([&] {
      print_point(pt1);
   });

   tasks.run([&] {
      print_point(pt2);
   });

   // Wait for the tasks to finish. If any task throws an exception, 
   // the runtime marshals it to the call to wait. 
   try
   {
      tasks.wait();
   }
   catch (const exception& e)
   {
      wcerr << L"Caught exception: " << e.what() << endl;
   }
}

Cet exemple génère la sortie suivante.

  

Pour obtenir un exemple complet qui utilise la gestion des exceptions dans un groupe de tâches, consultez Comment : utiliser la gestion des exceptions pour rompre une boucle parallèle.

[Premières]

Exceptions levées par le runtime

Une exception peut résulter d'un appel au runtime. La plupart des types d'exception, exceptés concurrency::task_canceled et concurrency::operation_timed_out, indiquent une erreur de programmation. Ces erreurs sont en général irrécupérables, et par conséquent ne doivent être ni interceptées ni gérées par le code d'application. Nous vous suggérons d'intercepter ou de gérer les erreurs irrécupérables dans votre code d'application uniquement lorsque vous devez diagnostiquer des erreurs de programmation. Toutefois, la connaissance des types d'exceptions définis par le runtime peut vous aider à diagnostiquer les erreurs de programmation.

Le mécanisme de gestion des exceptions est identique pour les exceptions levées par le runtime et pour celles levées par des fonctions de travail. Par exemple, la fonction concurrency::receive enlève operation_timed_out lorsqu'elle ne reçoit pas de message durant la période spécifiée. Si receive lève une exception dans une fonction de travail que vous passez à un groupe de tâches, le runtime stocke cette exception et la marshale au contexte qui appelle task_group::wait, structured_task_group::wait, task_group::run_and_wait ou structured_task_group::run_and_wait.

L'exemple suivant utilise l'algorithme concurrency::parallel_invoke pour exécuter deux tâches en parallèle. La première tâche attend cinq secondes, puis envoie un message vers un tampon de messages. La deuxième tâche utilise la fonction receive pour attendre pendant trois secondes de recevoir un message du même tampon de messages. La fonction receive lève operation_timed_out si elle ne reçoit pas le message dans le délai imparti.

// eh-time-out.cpp 
// compile with: /EHsc
#include <agents.h>
#include <ppl.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
   single_assignment<int> buffer;
   int result;

   try
   {
      // Run two tasks in parallel.
      parallel_invoke(
         // This task waits 5 seconds and then sends a message to  
         // the message buffer.
         [&] {
            wait(5000); 
            send(buffer, 42);
         },
         // This task waits 3 seconds to receive a message. 
         // The receive function throws operation_timed_out if it does  
         // not receive a message in the specified time period.
         [&] {
            result = receive(buffer, 3000);
         }
      );

      // Print the result.
      wcout << L"The result is " << result << endl;
   }
   catch (operation_timed_out&)
   {
      wcout << L"The operation timed out." << endl;
   }
}

Cet exemple génère la sortie suivante.

  

Pour empêcher tout arrêt anormal de votre application, assurez-vous que votre code gère les exceptions lorsqu'il fait appel au runtime. Gérez également les exceptions lorsque vous appelez du code externe qui utilise le runtime d'accès concurrentiel, par exemple une bibliothèque tierce.

[Premières]

Exceptions multiples

Si une tâche ou un algorithme parallèle reçoit plusieurs exceptions, le runtime marshale une seule de ces exceptions au contexte appelant. Le runtime ne garantit pas quelle exception il marshale.

L'exemple suivant utilise l'algorithme parallel_for pour imprimer des nombres sur la console. Il lève une exception si la valeur d'entrée est inférieure à une certaine valeur minimale ou supérieure à une certaine valeur maximale. Dans cet exemple, plusieurs fonctions de travail peuvent lever une exception.

// eh-multiple.cpp 
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   const int min = 0;
   const int max = 10;

   // Print values in a parallel_for loop. Use a try-catch block to  
   // handle any exceptions that occur in the loop. 
   try
   {
      parallel_for(-5, 20, [min,max](int i)
      {
         // Throw an exeception if the input value is less than the  
         // minimum or greater than the maximum. 

         // Otherwise, print the value to the console. 

         if (i < min)
         {
            stringstream ss;
            ss << i << ": the value is less than the minimum.";
            throw exception(ss.str().c_str());
         }
         else if (i > max)
         {
            stringstream ss;
            ss << i << ": the value is greater than than the maximum.";
            throw exception(ss.str().c_str());
         }
         else
         {
            wstringstream ss;
            ss << i << endl;
            wcout << ss.str();
         }
      });
   }
   catch (exception& e)
   {
      // Print the error to the console.
      wcerr << L"Caught exception: " << e.what() << endl;
   }  
}

Voici un exemple de sortie pour cet exemple.

  

[Premières]

Annulation

Les exceptions n'indiquent pas toutes une erreur. Par exemple, un algorithme de recherche peut utiliser la gestion des exceptions pour arrêter sa tâche associée lorsqu'il trouve le résultat. Pour plus d'informations sur la façon d'utiliser des mécanismes d'annulation dans votre code, consultez Annulation dans la bibliothèque de modèles parallèles.

[Premières]

Tâches légères

Une tâche légère est une tâche que vous planifiez directement à partir d'un objet concurrency::Scheduler. Les tâches légères présentent des charges inférieures aux tâches ordinaires. En revanche, le runtime n'intercepte pas les exceptions levées par des tâches légères. Au lieu de cela, l'exception est interceptée par le gestionnaire d'exceptions non gérée, qui par défaut met fin au processus. Par conséquent, vous devez utiliser un mécanisme de gestion des erreurs approprié dans votre application. Pour plus d'informations sur les tâches légères, consultez Planificateur de tâches (runtime d'accès concurrentiel).

[Premières]

Agents asynchrones

Comme les tâches légères, le runtime ne gère pas les exceptions levées par les agents asynchrones.

L'exemple suivant illustre une manière de gérer les exceptions dans une classe qui dérive de concurrency::agent. Cet exemple définit la classe points_agent. La méthode points_agent::run lit les objets point à partir du tampon de messages et les imprime sur la console. La méthode run lève une exception si elle reçoit un pointeur NULL.

La méthode run entoure tout le travail dans un bloc try-catch. Le bloc catch stocke l'exception dans un tampon de messages. L'application vérifie si l'agent a rencontré une erreur en lisant cette mémoire tampon une fois que l'agent a terminé.

// eh-agents.cpp 
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace concurrency;
using namespace std;

// Defines a point with x and y coordinates. 
struct point
{
   int X;
   int Y;
};

// Informs the agent to end processing.
point sentinel = {0,0};

// An agent that prints point objects to the console. 
class point_agent : public agent
{
public:
   explicit point_agent(unbounded_buffer<point*>& points)
      : _points(points)
   { 
   }

   // Retrieves any exception that occurred in the agent. 
   bool get_error(exception& e)
   {
      return try_receive(_error, e);
   }

protected:
   // Performs the work of the agent. 
   void run()
   {
      // Perform processing in a try block. 
      try
      {
         // Read from the buffer until we reach the sentinel value. 
         while (true)
         {
            // Read a value from the message buffer.
            point* r = receive(_points);

            // In this example, it is an error to receive a  
            // NULL point pointer. In this case, throw an exception. 
            if (r == NULL)
            {
               throw exception("point must not be NULL");
            }
            // Break from the loop if we receive the  
            // sentinel value. 
            else if (r == &sentinel)
            {
               break;
            }
            // Otherwise, do something with the point. 
            else
            {
               // Print the point to the console.
               wcout << L"X: " << r->X << L" Y: " << r->Y << endl;
            }
         }
      }
      // Store the error in the message buffer. 
      catch (exception& e)
      {
         send(_error, e);
      }

      // Set the agent status to done.
      done();
   }

private:
   // A message buffer that receives point objects.
   unbounded_buffer<point*>& _points;

   // A message buffer that stores error information.
   single_assignment<exception> _error;
};

int wmain()
{  
   // Create a message buffer so that we can communicate with 
   // the agent.
   unbounded_buffer<point*> buffer;

   // Create and start a point_agent object.
   point_agent a(buffer);
   a.start();

   // Send several points to the agent.
   point r1 = {10, 20};
   point r2 = {20, 30};
   point r3 = {30, 40};

   send(buffer, &r1);
   send(buffer, &r2);
   // To illustrate exception handling, send the NULL pointer to the agent.
   send(buffer, reinterpret_cast<point*>(NULL));
   send(buffer, &r3);
   send(buffer, &sentinel);

   // Wait for the agent to finish.
   agent::wait(&a);

   // Check whether the agent encountered an error.
   exception e;
   if (a.get_error(e))
   {
      cout << "error occurred in agent: " << e.what() << endl;
   }

   // Print out agent status.
   wcout << L"the status of the agent is: ";
   switch (a.status())
   {
   case agent_created:
      wcout << L"created";
      break;
   case agent_runnable:
      wcout << L"runnable";
      break;
   case agent_started:
      wcout << L"started";
      break;
   case agent_done:
      wcout << L"done";
      break;
   case agent_canceled:
      wcout << L"canceled";
      break;
   default:
      wcout << L"unknown";
      break;
   }
   wcout << endl;
}

Cet exemple génère la sortie suivante.

  

Étant donné que le bloc try-catch existe à l'extérieur de la boucle while, l'agent met fin au traitement lorsqu'il rencontre la première erreur. Si le bloc try-catch se trouvait à l'intérieur de la boucle while, l'agent continuerait après qu'une erreur se soit produite.

Cet exemple stocke les exceptions dans un tampon de messages afin qu'un autre composant puisse surveiller la présence éventuelle d'erreurs sur l'agent lors de son exécution. Cet exemple utilise un objet concurrency::single_assignment pour stocker l'erreur. Dans le cas où un agent gère plusieurs exceptions, la classe single_assignment stocke uniquement le premier message qui lui est passé. Pour stocker uniquement la dernière exception, utilisez la classe concurrency::overwrite_buffer. Pour stocker toutes les exceptions, utilisez la classe concurrency::unbounded_buffer. Pour plus d'informations sur ces blocs de messages, consultez Blocs de messages asynchrones.

Pour plus d'informations sur les agents asynchrones, consultez Agents asynchrones.

[Premières]

Résumé

[Premières]

Voir aussi

Concepts

Concurrency Runtime

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

Algorithmes parallèles

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

Planificateur de tâches (runtime d'accès concurrentiel)

Agents asynchrones