Share via


Exception Handling in the Concurrency Runtime

The Concurrency Runtime uses C++ exception handling to communicate many kinds of errors. These errors include invalid use of the runtime, runtime errors such as failure to acquire a resource, and errors that occur in work functions that you provide to task groups. This topic describes how the runtime handles exceptions that are thrown by task groups, lightweight tasks, and asynchronous agents, and how to respond to exceptions in your applications.

Sections

  • Task Groups and Parallel Algorithms

  • Lightweight Tasks

  • Asynchronous Agents

  • Common Exceptions

  • Summary

Task Groups and Parallel Algorithms

This section describes how the runtime handles exceptions that are thrown by task groups. This section also applies to parallel algorithms such as Concurrency::parallel_for, because these algorithms build on task groups.

Warning

Make sure that you understand the effects that exceptions have on dependent tasks. For recommended practices about how to use exception handling with tasks or parallel algorithms, see the Understand how Cancellation and Exception Handling Affect Object Destruction section in the Best Practices in the Parallel Patterns Library topic.

For more information about task groups, see Task Parallelism (Concurrency Runtime). For more information about parallel algorithms, see Parallel Algorithms.

Exceptions Thrown by Work Functions

A work function is a lambda function, function object, or function pointer that is passed to the runtime. When you pass a work function to a task group, the runtime runs that work function on a separate context.

When you throw an exception in the body of a work function that you pass to a Concurrency::task_group or Concurrency::structured_task_group object, the runtime stores that exception and marshals it to the context that calls Concurrency::task_group::wait, Concurrency::structured_task_group::wait, Concurrency::task_group::run_and_wait, or Concurrency::structured_task_group::run_and_wait. The runtime also stops all active tasks that are in the task group (including those in child task groups) and discards any tasks that have not yet started.

The following example shows the basic structure of a work function that throws an exception. The example uses a task_group object to print the values of two point objects in parallel. The print_point work function prints the values of a point object to the console. The work function throws an exception if the input value is NULL. The runtime stores this exception and marshals it to the context that calls 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;
   }
}

This example produces the following output.

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

For a complete example that uses exception handling in a task group, see How to: Use Exception Handling to Break from a Parallel Loop.

Exceptions Thrown by the Runtime

In addition to work functions, an exception can result from a call to the runtime. Most exceptions that are thrown by the runtime indicate a programming error. These errors are typically unrecoverable, and therefore should not be caught or handled by application code. However, understanding the exception types that are defined by the runtime can help you diagnose programming errors. The section Common Exceptions describes the common exceptions and the conditions under which they occur.

The exception handling mechanism is the same for exceptions that are thrown by the runtime as exceptions that are thrown by work functions. For example, the Concurrency::receive function throws operation_timed_out when it does not receive a message in the specified time period. If receive throws an exception in a work function that you pass to a task group, the runtime stores that exception and marshals it to the context that calls task_group::wait, structured_task_group::wait, task_group::run_and_wait, or structured_task_group::run_and_wait.

The following example uses the Concurrency::parallel_invoke algorithm to run two tasks in parallel. The first task waits five seconds and then sends a message to a message buffer. The second task uses the receive function to wait three seconds to receive a message from the same message buffer. The receive function throws operation_timed_out if it does not receive the message in the time period.

// 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;
   }
}

This example produces the following output.

The operation timed out.

To prevent abnormal termination of your application, make sure that your code handles exceptions when it calls into the runtime. Also handle exceptions when you call into external code that uses the Concurrency Runtime, for example, a third-party library.

Multiple Exceptions

If a task or parallel algorithm receives multiple exceptions, the runtime marshals only one of those exceptions to the calling context. The runtime does not guarantee which exception it marshals.

The following example uses the parallel_for algorithm to print numbers to the console. It throws an exception if the input value is less than some minimum value or greater than some maximum value. In this example, multiple work functions can throw an 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;
   }  
}

The following shows sample output for this example.

8
2
9
3
10
4
5
6
7
Caught exception: -5: the value is less than the minimum.

Cancellation

Not all exceptions indicate an error. For example, a search algorithm might use exception handling to stop its associated task when it finds the result. For more information about how to use cancellation mechanisms in your code, see Cancellation in the PPL.

[go to top]

Lightweight Tasks

A lightweight task is a task that you schedule directly from a Concurrency::Scheduler object. Lightweight tasks carry less overhead than ordinary tasks. However, the runtime does not catch exceptions that are thrown by lightweight tasks. Instead, the exception is caught by the unhandled exception handler, which by default terminates the process. Therefore, use an appropriate error-handling mechanism in your application. For more information about lightweight tasks, see Task Scheduler (Concurrency Runtime).

[go to top]

Asynchronous Agents

Like lightweight tasks, the runtime does not manage exceptions that are thrown by asynchronous agents.

The following example shows one way to handle exceptions in a class that derives from Concurrency::agent. This example defines the points_agent class. The points_agent::run method reads point objects from the message buffer and prints them to the console. The run method throws an exception if it receives a NULL pointer.

The run method surrounds all work in a try-catch block. The catch block stores the exception in a message buffer. The application checks whether the agent encountered an error by reading from this buffer after the agent finishes.

// 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 occured 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 occured 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;
}

This example produces the following output.

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

Because the try-catch block exists outside the while loop, the agent ends processing when it encounters the first error. If the try-catch block was inside the while loop, the agent would continue after an error occurs.

This example stores exceptions in a message buffer so that another component can monitor the agent for errors as it runs. This example uses a Concurrency::single_assignment object to store the error. In the case where an agent handles multiple exceptions, the single_assignment class stores only the first message that is passed to it. To store only the last exception, use the Concurrency::overwrite_buffer class. To store all exceptions, use the Concurrency::unbounded_buffer class. For more information about these message blocks, see Asynchronous Message Blocks.

For more information about asynchronous agents, see Asynchronous Agents.

[go to top]

Common Exceptions

The following table lists the common exception classes in the Concurrency Runtime and provides the condition under which the exception is thrown. Most exception types, except for operation_timed_out and unsupported_os, indicate a programming error. These errors are typically unrecoverable, and therefore should not be caught or handled by application code. We suggest that you only catch or handle unrecoverable errors in your application code when you need to diagnose programming errors.

Exception class

Condition

bad_target

You pass an invalid pointer to a message block.

context_self_unblock

A context has tried to unblock itself.

context_unblock_unbalanced

The runtime tried to unblock a context, but that context is already unblocked.

default_scheduler_exists

A scheduler was attempted to be set as the default scheduler, but a default scheduler already exists.

improper_lock

A lock was incorrectly acquired.

improper_scheduler_attach

A context was attached to the same scheduler multiple times.

improper_scheduler_detach

A context that is internally managed by the runtime was detached from its scheduler, or the context is not attached to any scheduler.

improper_scheduler_reference

A context incremented the reference counter on a scheduler that is shutting down, and that context that is not internal to the scheduler.

invalid_link_target

The same object is linked to a message block more than one time.

invalid_multiple_scheduling

A task that has not completed has been scheduled more than one time.

invalid_operation

The runtime has performed an invalid operation.

invalid_oversubscribe_operation

Oversubscription was disabled when it was not enabled.

invalid_scheduler_policy_key

An invalid policy key was provided to a Concurrency::SchedulerPolicy object.

invalid_scheduler_policy_thread_specification

A SchedulerPolicy object is specified to have a maximum concurrency level that is less than the minimum concurrency level.

invalid_scheduler_policy_value

An invalid policy value was provided to a SchedulerPolicy object.

message_not_found

A message block was unable to find a requested message.

missing_wait

A task group object was destroyed before the Concurrency::task_group::wait method or the Concurrency::structured_task_group::wait method was called.

nested_scheduler_missing_detach

A nested scheduler did not correctly detach itself from the parent.

operation_timed_out

An operation did not complete in the specified time period.

scheduler_not_attached

A context tried to detach from its scheduler, but the context was not attached to any scheduler.

scheduler_resource_allocation_error

The runtime did not acquire a critical resource, for example, a resource that is provided by the operating system.

unsupported_os

The runtime is not supported on the current operating system.

[go to top]

Summary

When a task throws an exception, the runtime holds that exception and marshals it to the context that waits for the task group to finish. For components such as lightweight tasks and agents, the runtime does not manage exceptions for you. In these cases, you must implement your own exception-handling mechanism.

[go to top]

See Also

Concepts

Concurrency Runtime

Task Parallelism (Concurrency Runtime)

Parallel Algorithms

Cancellation in the PPL

Task Scheduler (Concurrency Runtime)

Asynchronous Agents

Change History

Date

History

Reason

March 2011

Added a cautionary note about the effects that exceptions can have on dependent tasks.

Information enhancement.