Share 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 types d’erreurs. Ces erreurs incluent l’utilisation non valide du runtime, les erreurs d’exécution telles que l’échec de l’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 contient cette exception et le marshale dans le contexte qui attend que la tâche ou le groupe de tâches se termine. Pour les composants tels que les tâches et les agents légers, le runtime ne gère pas les exceptions pour vous. Dans ces cas, 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 les tâches, les groupes de tâches, les tâches légères et les 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 contient cette exception et le marshale dans le contexte qui attend que la tâche ou le groupe de tâches se termine.

  • Lorsque cela est possible, entourez chaque appel à concurrency ::task ::get et concurrency ::task ::wait avec un try/catch bloc pour gérer les erreurs à partir de laquelle vous pouvez récupérer. Le runtime met fin à l’application si une tâche lève une exception et que cette exception n’est pas interceptée par la tâche, l’une de ses continuations ou l’application principale.

  • Une continuation basée sur des tâches s’exécute toujours ; il n’importe pas si la tâche antécédente s’est terminée avec succès, a levé une exception ou a été annulée. Une continuation basée sur des valeurs ne s’exécute pas si la tâche antécédente lève ou annule.

  • Étant donné que les continuations basées sur des tâches s’exécutent toujours, déterminez s’il faut ajouter une continuation basée sur des tâches à la fin de votre chaîne de continuation. Cela peut vous aider à garantir que votre code observe toutes les exceptions.

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

  • Le runtime ne gère pas les exceptions pour les tâches et agents légers.

Dans ce document

Tâches et continuations

Cette section décrit comment le runtime gère les exceptions levées par les objets concurrency ::task et leurs continuations. Pour plus d’informations sur le modèle de tâche et de continuation, consultez Le parallélisme des tâches.

Lorsque vous lèvez une exception dans le corps d’une fonction de travail que vous passez à un task objet, le runtime stocke cette exception et le marshale dans le contexte qui appelle concurrency ::task ::get ou concurrency ::task ::wait. Le parallélismedes tâches décrit les continuations basées sur les tâches et les valeurs, mais pour résumer, une continuation basée sur une valeur prend un paramètre de type T et une continuation basée sur des tâches prend un paramètre de typetask<T>. Si une tâche qui lève une ou plusieurs continuations basées sur des valeurs, ces continuations 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.
*/

Une continuation basée sur des tâches vous permet de gérer toutes les exceptions levées par la tâche antécédente. Une continuation basée sur des tâches s’exécute toujours ; il n’importe pas si la tâche s’est terminée correctement, a levé une exception ou a été annulée. Lorsqu’une tâche lève une exception, ses continuations basées sur des tâches sont planifiées pour s’exécuter. L’exemple suivant montre une tâche qui lève toujours. La tâche a deux continuations ; l’une est basée sur la valeur et l’autre est basée sur des tâches. L’exception basée sur les tâches s’exécute toujours et peut donc intercepter l’exception levée par la tâche antécédente. Lorsque l’exemple attend que les deux continuations se terminent, l’exception est levée à nouveau, car l’exception de tâche est toujours levée quand task::get ou task::wait est appelée.

// 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 d’utiliser des continuations basées sur des tâches pour intercepter les exceptions que vous pouvez gérer. Étant donné que les continuations basées sur des tâches s’exécutent toujours, déterminez s’il faut ajouter une continuation basée sur des tâches à la fin de votre chaîne de continuation. Cela peut vous aider à garantir que votre code observe toutes les exceptions. L’exemple suivant montre une chaîne de continuation basée sur des valeurs de base. La troisième tâche de la chaîne lève, et par conséquent toutes les continuations basées sur des valeurs qui suivent ne sont pas exécutées. Toutefois, la continuation finale est basée sur les tâches et s’exécute donc toujours. Cette continuation finale gère l’exception levée par la troisième tâche.

Nous vous recommandons d’intercepter les exceptions les plus spécifiques que vous pouvez. Vous pouvez omettre cette continuation finale basée sur des tâches si vous n’avez pas d’exceptions spécifiques à intercepter. Toute exception reste non gérée 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 task-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 à un événement d’achèvement de tâche. Le parallélisme des tâches de document décrit plus en détail la classe concurrency ::task_completion_event.

concurrency ::task_canceled est un type d’exception d’exécution important qui se rapporte à task. Le runtime lève task_canceled lorsque vous appelez task::get et que cette tâche est annulée. (À l’inverse, task::wait retourne task_status ::canceled et ne lève pas.) Vous pouvez intercepter et gérer cette exception à partir d’une continuation basée sur des tâches ou lorsque vous appelez task::get. Pour plus d’informations sur l’annulation des tâches, consultez Annulation dans la bibliothèque PPL.

Attention

Ne levez jamais task_canceled à partir de votre code. Appelez la concurrence ::cancel_current_task à la place.

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

La section Exceptions levées par le runtime dans ce document décrit comment utiliser plus en détail les exceptions d’exécution.

[Haut]

Groupes de tâches et algorithmes parallèles

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

Attention

Veillez à comprendre les effets que les exceptions ont sur les tâches dépendantes. Pour connaître les pratiques recommandées sur l’utilisation de la gestion des exceptions avec des tâches ou des algorithmes parallèles, 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.

Pour plus d’informations sur les groupes de tâches, consultez Parallélisme des tâches. Pour plus d’informations sur les algorithmes parallèles, consultez Algorithmes parallèles.

Lorsque vous lèvez 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 le marshale dans le 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 se trouvent dans le groupe de tâches (y compris celles des groupes de tâches enfants) et dés carte 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 task_group objet pour imprimer les valeurs de deux point objets en parallèle. La print_point fonction de travail imprime les valeurs d’un point objet dans la console. La fonction de travail lève une exception si la valeur d’entrée est NULL. Le runtime stocke cette exception et le marshale dans le 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 produit la sortie suivante.

X = 15, Y = 30Caught exception: point is NULL.

Pour obtenir un exemple complet qui utilise la gestion des exceptions dans un groupe de tâches, consultez How to : Use Exception Handling to Break from a Parallel Loop.

[Haut]

Exceptions levées par le runtime

Une exception peut résulter d’un appel au runtime. La plupart des types d’exceptions, à l’exception de concurrency ::task_canceled et concurrency ::operation_timed_out, indiquent une erreur de programmation. Ces erreurs sont généralement irrécupérables et ne doivent donc pas être interceptées ou gérées par le code de l’application. Nous vous suggérons d’intercepter ou de gérer uniquement les erreurs irrécupérables dans votre code d’application lorsque vous devez diagnostiquer les erreurs de programmation. Toutefois, la compréhension 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 le même pour les exceptions levées par le runtime que les exceptions levées par les fonctions de travail. Par exemple, la fonction concurrency ::receive lève operation_timed_out lorsqu’elle ne reçoit pas de message dans 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 le marshale dans le contexte qui appelle task_group::wait, structured_task_group::waitou structured_task_group::run_and_waittask_group::run_and_wait.

L’exemple suivant utilise l’algorithme concurrency ::p arallel_invoke pour exécuter deux tâches en parallèle. La première tâche attend cinq secondes, puis envoie un message à une mémoire tampon de message. La deuxième tâche utilise la receive fonction pour attendre trois secondes pour recevoir un message à partir du même tampon de message. La receive fonction lève operation_timed_out si elle ne reçoit pas le message au cours de la période.

// 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 produit la sortie suivante.

The operation timed out.

Pour éviter l’arrêt anormal de votre application, assurez-vous que votre code gère les exceptions lorsqu’il appelle le 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.

[Haut]

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 dans la console. Elle lève une exception si la valeur d’entrée est inférieure à une valeur minimale ou supérieure à une 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;
   }  
}

L’exemple de sortie suivant montre l’exemple de sortie de cet exemple.

8293104567Caught exception: -5: the value is less than the minimum.

[Haut]

Annulation

Toutes les exceptions n’indiquent pas 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 l’utilisation de mécanismes d’annulation dans votre code, consultez Annulation dans la bibliothèque PPL.

[Haut]

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 entraînent moins de surcharge que les tâches ordinaires. Toutefois, 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é, qui met par défaut fin au processus. Par conséquent, utilisez 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.

[Haut]

Agents asynchrones

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

L’exemple suivant montre une façon de gérer les exceptions dans une classe qui dérive de concurrency ::agent. Cet exemple définit la points_agent classe. La points_agent::run méthode lit les point objets de la mémoire tampon de message et les imprime dans la console. La run méthode lève une exception si elle reçoit un NULL pointeur.

La run méthode entoure tout le travail dans un try-catch bloc. Le catch bloc stocke l’exception dans une mémoire tampon de message. L’application case activée si l’agent a rencontré une erreur en lisant cette mémoire tampon une fois l’agent 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 produit la sortie suivante.

X: 10 Y: 20
X: 20 Y: 30
error occurred in agent: point must not be NULL
the status of the agent is: done

Étant donné que le try-catch bloc existe en dehors de la while boucle, l’agent termine le traitement lorsqu’il rencontre la première erreur. Si le try-catch bloc était à l’intérieur de la while boucle, l’agent continue après qu’une erreur se produit.

Cet exemple stocke des exceptions dans une mémoire tampon de message afin qu’un autre composant puisse surveiller l’agent pour les erreurs 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 single_assignment classe stocke uniquement le premier message qui lui est transmis. 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.

[Haut]

Résumé

[Haut]

Voir aussi

Concurrency Runtime
Parallélisme des tâches
Algorithmes parallèles
Annulation dans la bibliothèque de modèles parallèles
Planificateur de tâches
Agents asynchrones