Parallelisation Talk examples – Cancelling Tasks

This example showed what happens when tasks are cancelled. In this example, some tasks will be able to run to completion, others will be cancelled and other won’t even get a chance to start because the cancellation token was signalled before the task gets a chance to start.

Here is the code for the cancellation example shown in the talk

class Program
{
    static void Main(string[] args)
    {
        const int numTasks = 9;

        // Set up the cancellation source and get the token.
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        CancellationToken token = tokenSource.Token;

        // Set up the tasks
        Task[] tasks = new Task[numTasks];
        for (int i = 0; i < numTasks; i++)
            tasks[i] = Task.Factory.StartNew(() => PerformTask(token), token);

        // Now the tasks are all set up, show the state.
        // Most will be WaitingToRun, some will be Running
        foreach (Task t in tasks.OrderBy(t => t.Id))
            Console.WriteLine("Tasks {0} state: {1}", t.Id, t.Status);

        // Give some of the tasks a chance to do something.
        Thread.Sleep(1500);

        // Cancel the tasks
        Console.WriteLine("Cancelling tasks");
        tokenSource.Cancel();
        Console.WriteLine("Cancellation Signalled");

        try
        {
            // Wait for the tasks to cancel if they've not already completed
            Task.WaitAll(tasks);
        }
        catch (AggregateException aex)
        {
            aex.Handle(ex =>
            {
                // Handle the cancelled tasks
                TaskCanceledException tcex = ex as TaskCanceledException;
                if (tcex != null)
                {
                    Console.WriteLine("Handling cancellation of task {0}", tcex.Task.Id);
                    return true;
                }

                // Not handling any other types of exception.
                return false;
            });
        }

        // Show the state of each of the tasks.
        // Some will be RanToCompletion, others will be Cancelled.
        foreach(Task t in tasks.OrderBy(t => t.Id))
            Console.WriteLine("Tasks {0} state: {1}", t.Id, t.Status);


        Console.WriteLine("Program End");
        Console.ReadLine();
    }

    static void PerformTask(CancellationToken token)
    {
        try
        {
            // The loop simulates work that can be cooperatively cancelled.
            Console.WriteLine("Task {0}: Starting", Task.CurrentId);
            for (int i = 0; i < 4; i++)
            {
                // Check for the cancellation to be signalled
                token.ThrowIfCancellationRequested();

                // Write out a little bit showing the progress of the task
                Console.WriteLine("Task {0}: {1}/4 In progress", Task.CurrentId, i + 1);
                Thread.Sleep(500); // Simulate doing some work
            }
            // By getting here the task will RunToCompletion even if the
            // token has been signalled.
            Console.WriteLine("Task {0}: Finished", Task.CurrentId);
        }
        catch (OperationCanceledException)
        {
            // Any clean up code goes here.
            Console.WriteLine("Task {0}: Cancelling", Task.CurrentId);
            throw; // To ensure that the calling code knows the task was cancelled.
        }
        catch(Exception)
        {
            // Clean up other stuff
            throw; // If the calling code also needs to know.
        }
    }
}

 

Here is the output of the program (your results may vary):

Task 1: Starting
Task 1: 1/4 In progress
Task 2: Starting
Task 2: 1/4 In progress
Tasks 1 state: Running
Task 3: Starting
Task 3: 1/4 In progress
Tasks 2 state: Running
Task 4: Starting
Task 4: 1/4 In progress
Tasks 3 state: Running
Tasks 4 state: Running
Tasks 5 state: WaitingToRun
Tasks 6 state: WaitingToRun
Tasks 7 state: WaitingToRun
Tasks 8 state: WaitingToRun
Tasks 9 state: WaitingToRun
Task 1: 2/4 In progress
Task 2: 2/4 In progress
Task 3: 2/4 In progress
Task 4: 2/4 In progress
Task 1: 3/4 In progress
Task 2: 3/4 In progress
Task 4: 3/4 In progress
Task 3: 3/4 In progress
Task 1: 4/4 In progress
Task 2: 4/4 In progress
Task 4: 4/4 In progress
Task 3: 4/4 In progress
Task 5: Starting
Task 5: 1/4 In progress

To this point the tasks have been given a chance to operate normally. The tasks that have started are outputing to the console their progress. The main thread reports on the state of the tasks and shows tasks 1 to 4 are Running while the remainder are WaitingToRun. After a while the scheduler decides to start task 5.

Next the tasks are going to be cancelled.

Cancelling tasks
Cancellation Signalled
Task 1: Finished
Task 2: Finished
Task 4: Finished
Task 3: Finished
Task 5: Cancelling

When the cancellation token is signalled the tasks have to cooperate. Tasks 1 to 4 are too far gone and will run to completion. Task 5, which was only just started, cooperates with the cancellation request and writes that it is cancelling. No waiting tasks are started.

In the main thread, the control is blocked until all the tasks have either finished or cooperate with the cancellation request. Once the WaitAll unblocks the program handles any cancelled tasks in the catch block.

Handling cancellation of task 9
Handling cancellation of task 8
Handling cancellation of task 7
Handling cancellation of task 6
Handling cancellation of task 5

Tasks 6 to 9 never got a chance to start. Task 5 was started, but was cancelled. Therefore task 5′s cancellation can be handled inside the task and outside it. Different clean up may be required in each place.

Finally, the program lists the end state (See also: Task state transitions) of each of the tasks:

Tasks 1 state: RanToCompletion
Tasks 2 state: RanToCompletion
Tasks 3 state: RanToCompletion
Tasks 4 state: RanToCompletion
Tasks 5 state: Canceled
Tasks 6 state: Canceled
Tasks 7 state: Canceled
Tasks 8 state: Canceled
Tasks 9 state: Canceled
Program End

When writing code to handle cancelled tasks, watch out for this gotcha that can trip you up if you are not careful.

Task status state changes

Over the course of the last couple of months I’ve blogged a lot about the Task Parallel Library and mentioned a number of statuses that a task can have, but nowhere is a nice handy-dandy chart to show what those statuses are and how the transition from one to another. The green status show the normal line that most tasks will take. The purple and red show alternative paths that may be taken.

When a task is first created it status is Created – however you will almost never see this. This status only exists when you create a task using its constructor. Most of the time you would create tasks with Task.Factory.StartNew(…)

When you start a task the status will be WaitingToRun until such time as the task scheduler can actually run the task. For some tasks the transition will be almost instant, for others it will mean a wait while other tasks complete.

From WaitingToRun a task can transition to either Running or Cancelled. The latter transition happens if the cancellation token is signalled before the task gets a chance to start.

Once a task is running there are three possible exits for it. The normal exit is for the status to transition to RanToCompletion which means that the task completed without incident. The other two exits require exception handling in the calling code.

If an error happens that the task cannot (or does not) handle internally then it will transition to Faulted (see Tasks that throw exceptions). The AggregateException will contain details of all the exceptions in Faulted tasks as part of its InnerExceptions collection.

If the tasks are cancelled, any task that has not already completed will transition immediately to Cancelled. An AggregateException will contain details of all tasks that were cancelled (whether they had a chance to run or not) with TaskCanceledException inner exceptions. The exception to this is any task that was running already when the cancellation token was signalled and either ran to completion normally, or did not respond correctly to the cancellation request (see Cancelling parallel tasks).

Cancelling parallel tasks

UPDATE (7-June-2011): The post as it originally appeared had a bug in the code, the catch block in the task caught the wrong exception type. See the Gotcha section at the end for an explanation on why there are two types of exception for this.

I think, to date, I’ve mentioned most of the task lifecycle, but I’ve not talked about cancelling tasks yet. So here goes.

You can cancel tasks for what ever reason by passing in a cancellation token to the task. The task must be cooperative insomuch as it must watch the cancellation token to detect if a cancellation has been signalled then it can clean up and exit.

The basic program

So, the little example program to demonstrate this is this:

class Program
{
    static void Main(string[] args)
    {
        const int numTasks = 9;
        Task[] tasks = new Task[numTasks];
        for (int i = 0; i < 10; i++)
            tasks[i] = Task.Factory.StartNew(PerformTask);

        Task.WaitAll(tasks);

        foreach(Task t in tasks)
            Console.WriteLine("Tasks {0} state: {1}", t.Id, t.Status);

        Console.WriteLine("Program End");
        Console.ReadLine();
    }

    static void PerformTask()
    {
        Console.WriteLine("Task {0}: Starting", Task.CurrentId);
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine("Task {0}: {1}/3 In progress", Task.CurrentId, i+1);
            Thread.Sleep(500); // Simulate doing some work
        }
        Console.WriteLine("Task {0}: Finished", Task.CurrentId);
    }
}

So far this doesn’t do much. It starts 9 tasks and each runs to completion. Each task’s end state is RanToCompletion.

Setting up the CancellationToken

Now, if we introduce the cancellation token to the task we can cancel the task at some point during its execution. The Main method then gets changed to this:

static void Main(string[] args)
{
    const int numTasks = 9;

    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;

    Task[] tasks = new Task[numTasks];
    for (int i = 0; i < numTasks; i++)
        tasks[i] = Task.Factory.StartNew(() => PerformTask(token), token);

    Thread.Sleep(1500);
    Console.WriteLine("Cancelling tasks");
    tokenSource.Cancel();
    Console.WriteLine("Cancellation Signalled");

    Task.WaitAll(tasks);

    foreach(Task t in tasks)
        Console.WriteLine("Tasks {0} state: {1}", t.Id, t.Status);


    Console.WriteLine("Program End");
    Console.ReadLine();
}

The PerformTask method now takes a CancellationToken (but doesn’t yet do anything with it)

If this code is run, the Task.WaitAll method call will throw an AggregateException with a number of TaskCanceledException objects.

Handling cancelled tasks

You therefore have to surround your WaitAll method with a try/catch block and look out for TaskCanceledException objects and handle them as you need (see also: handling AggregateException exceptions). In my example I’m just going to output the fact to the console. The try/catch block looks like this:

try
{
    Task.WaitAll(tasks);
}
catch (AggregateException aex)
{
    aex.Handle(ex =>
    {
        TaskCanceledException tcex = ex as TaskCanceledException;
        if (tcex != null)
        {
            Console.WriteLine("Handling cancellation of task {0}", tcex.Task.Id);
            return true;
        }
        return false;
    });
}

The tasks that were in progress at the time the cancel was signalled complete as normal. Cancelling the tasks will not stop any currently running task.

Responding to a cancellation request: IsCancellationRequested

If the tasks are sufficiently short running allowing them to complete may be perfectly acceptable.

However, if a task is long running or it is safe to cancel them then you can allow your task to cooperate and respond to the token being signalled to cancel.

The CancellationToken object has a property you can check called IsCancellationRequested. For example:

static void PerformTask(CancellationToken token)
{
    Console.WriteLine("Task {0}: Starting", Task.CurrentId);
    for (int i = 0; i < 4; i++)
    {
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Task {0}: Cancelling", Task.CurrentId);
            return;
        }
        Console.WriteLine("Task {0}: {1}/3 In progress", Task.CurrentId, i+1);
        Thread.Sleep(500); // Simulate doing some work
    }
    Console.WriteLine("Task {0}: Finished", Task.CurrentId);
}

If you simply exit from your task, like the above example, then the Status of the task will be RanToCompletion as if the task completed normally. If you do not need to know whether a task actually completed or was cancelled then this may be completely acceptable.

Responding to a cancellation request: ThrowIfCancellationRequested

If you need to perform clean up or the calling code needs to know that a task has been cancelled then using the CancellationToken’s ThrowIfCancellationRequested() method may be a better choice.

If you do need to perform clean up inside your task, ensure that the OperationCanceledExcption is thrown again so that the calling code knows that the task was cancelled.

static void PerformTask(CancellationToken token)
{
    try
    {
        Console.WriteLine("Task {0}: Starting", Task.CurrentId);
        for (int i = 0; i < 4; i++)
        {
            token.ThrowIfCancellationRequested();
            Console.WriteLine("Task {0}: {1}/3 In progress", Task.CurrentId, i + 1);
            Thread.Sleep(500); // Simulate doing some work
        }
        Console.WriteLine("Task {0}: Finished", Task.CurrentId);
    }
    catch (OperationCanceledException)
    {
        // Any clean up code goes here.
        Console.WriteLine("Task {0}: Cancelling", Task.CurrentId);
        throw; // To ensure that the calling code knows the task was cancelled.
    }
    catch(Exception)
    {
        // Clean up other stuff
        throw; // If the calling code also needs to know.
    }
}

Remember that if you allow other exceptions to escape your task then the task’s status will be Faulted.

Gotcha!

This section was added on 7-June-2011.

One thing to watch out for is that the exception you get inside the task is different from the exception you get inside the AggregateException outside the task. Normally, you’d expect that the exception is passed through and becomes one of the InnerExceptions in the aggregate exceptions.

It you want to keep the code consistent and only deal with one exception type for cancelled tasks you can simple deal with the OperationCanceledException throughout (both inside and outside the tasks) as that is the base class. Outside the task the exception object is actually a TaskCanceledException.

The advantage of referencing the more specific TaskCanceledException outside the task is that the exception object also contains a reference to the Task that was cancelled. Inside the task the exception that ThrowIfCancellationRequested throws is an OperationCanceledException (which doesn’t contain the Task object, however you are inside the task at this point)

The other point to note is that outside the task, the TaskCanceledException object in the AggregateException object doesn’t contain much of the information you’d expect to find in an Exception object (such as a Stack Trace).

Handling AggregateExceptions

I’ve written a couple of posts (see also) about how the AggregateException plays its part in exception handling in parallel systems. However, it has another trick up its sleeve when it comes to handling exceptions.

AggregateException has a Handle method that takes a delegate of Func<Exception, bool> i.e. It takes an Exception as a parameter and returns a bool. The return value indicates whether the function handled the exception or not.

Here is an example program that shows what how it works.

class Program
{
    static void Main(string[] args)
    {
        try
        {
            DoWork();
        }
        catch(AggregateException aex)
        {
            Console.WriteLine("Handle Remaining Exceptions");
            foreach(Exception ex in aex.InnerExceptions)
            {
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);
            }
        }

        Console.ReadLine();
    }

    private static void DoWork()
    {
        const int numTasks = 20;
        Task[] tasks = new Task[numTasks];
        for (int i = 0; i < numTasks; i++)
            tasks[i] = Task.Factory.StartNew(PerformTask);

        Thread.Sleep(2500);

        try
        {
            Task.WaitAll(tasks);
        }
        catch(AggregateException aex)
        {
            Console.WriteLine("AggregateException.Handle...");
            aex.Handle(ex => HandleException(ex));
            Console.WriteLine("Finished handling exceptions."); // This never shows
        }
    }

    public static bool HandleException(Exception ex)
    {
        if (ex is OddException)
        {
            Console.WriteLine("Handling: {0}", ex.Message);
            return true;
        }

        Console.WriteLine("Not handling: {0}", ex.Message);
        return false;
    }

    public static void PerformTask()
    {
        int? id = Task.CurrentId;
        Console.WriteLine("Performing Task {0}", id);

        if (id.Value % 13 == 0)
            throw new TriskaidekaException("Mwaaahaahaahaahaaaaaaaa!");

        if (id.Value % 2 == 1)
            throw new OddException("The task ("+id+") is distinctly odd.");
    }
}

The program starts 20 tasks (DoWork). Each task (PerformTask) simply outputs a line to the console to say what it’s id is and then throws an exception depending on some condition. There are two types of exception that it can throw.

Back in the main thread (DoWork) a Sleep statement gives the tasks a chance to get going (and hopefully complete). During this time, the tasks get the opportunity to output the following.

Performing Task 1
Performing Task 2
Performing Task 4
Performing Task 3
Performing Task 5
Performing Task 8
Performing Task 9
Performing Task 10
Performing Task 12
Performing Task 13
Performing Task 6
Performing Task 14
Performing Task 7
Performing Task 16
Performing Task 17
Performing Task 18
Performing Task 15
Performing Task 11
Performing Task 19
Performing Task 20

Then the Task.WaitAll statement is called which will potentially throw an AggregateException (in fact it will with 10 InnerExceptions).

Since the Task.WaitAll call is wrapped in a try/catch the AggregateException is caught. We output a message to say the exceptions are being handled. The AggregateException.Handle method calls the method given (HandleException) once for each of the InnerExceptions.

AggregateException.Handle...

The HandleException method either handles the exception (in which case it returns true) or it doesn’t (so returning false). In each case it also writes to the console to say what it has done. That console output looks like this:

Handling: The task (19) is distinctly odd.
Handling: The task (17) is distinctly odd.
Handling: The task (15) is distinctly odd.
Not handling: Mwaaahaahaahaahaaaaaaaa!
Handling: The task (11) is distinctly odd.
Handling: The task (9) is distinctly odd.
Handling: The task (7) is distinctly odd.
Handling: The task (5) is distinctly odd.
Handling: The task (3) is distinctly odd.
Handling: The task (1) is distinctly odd.

The AggregateException.Handle method then checks to see if any of the exceptions remain unhandled. If there are still unhandled exceptions then it rethrows. Since there is one remaining exception that is unhandled the line of code after the call to Handle is never called.

The try/catch block in the Main method catches AggregateException and just loops over the remaining exceptions to show what was left unhandled.

Handle Remaining Exceptions
TriskaidekaException: Mwaaahaahaahaahaaaaaaaa!

See also

Tasks that throw exceptions

I’ve blogged before about the AggregateException in .NET 4, but I missed out something that may be important. If you are using Parallel.Invoke or Parallel.For or Parallel.ForEach or PLINQ you probably won’t notice this because each of these constructs block until all the tasks are completed. However, if you are using Task.Factory.StartNew() then this may be important.

The AggregateException won’t bubble up into the calling thread or task until one of the Wait… methods (excluding WaitAny) is called.

This means that any number of exceptions can be happening but the thread that starts the task may not know about it for some time if it is getting on with other things itself.

Take this code for example:

class Program
{
    static void Main(string[] args)
    {
        // Start the tasks
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            Task t = Task.Factory.StartNew(PerformTask);
            tasks.Add(t);
        }

        Console.WriteLine("Press enter to display the task status.");
        Console.ReadLine();

        // Display the status of each task.
        // If it has thrown an exception will be "faulted"
        foreach(Task t in tasks)
            Console.WriteLine("Task {0} status: {1}", t.Id, t.Status);

        Console.WriteLine("Press enter to wait for all tasks.");
        Console.ReadLine();

        // This is where the AggregateException is finally thrown
        Task.WaitAll(tasks.ToArray());

        Console.ReadLine();
    }

    public static void PerformTask()
    {
        Console.WriteLine("Starting Task {0}", Task.CurrentId);
        throw new Exception("Throwing exception in task "+Task.CurrentId);
    }
}

At the start of the program it starts 20 tasks, each of which throws an exception. The output of the program at this point is:

Press enter to display the task status.
Starting Task 2
Starting Task 1
Starting Task 4
Starting Task 3
Starting Task 5
Starting Task 7
Starting Task 6
Starting Task 8
Starting Task 9
Starting Task 10
Starting Task 11
Starting Task 12
Starting Task 13
Starting Task 14
Starting Task 15
Starting Task 17
Starting Task 16
Starting Task 18
Starting Task 19
Starting Task 20

The reason the "Press enter to display the task status" appears on the first line is that the main thread is still running and performing operations as the background tasks are still spinning up.

Then each of the tasks are started and each output a simple line to the console to prove they are there. Then each throws an exception, so far unseen (other than for the debugger breaking the running on the program to tell the developer, but in a production system you are not going to have that.)

Now, the application is paused on a Console.ReadLine() waiting for the user to press enter. The application is still running merrily.

If the enter key is pressed the next bit of output is displayed:

Task 1 status: Faulted
Task 2 status: Faulted
Task 3 status: Faulted
Task 4 status: Faulted
Task 5 status: Faulted
Task 6 status: Faulted
Task 7 status: Faulted
Task 8 status: Faulted
Task 9 status: Faulted
Task 10 status: Faulted
Task 11 status: Faulted
Task 12 status: Faulted
Task 13 status: Faulted
Task 14 status: Faulted
Task 15 status: Faulted
Task 16 status: Faulted
Task 17 status: Faulted
Task 18 status: Faulted
Task 19 status: Faulted
Task 20 status: Faulted
Press enter to wait for all tasks.

This time the "Press enter to wait for all tasks." message appears at the end of the list. This is because everything here is being written from the main thread.

As you can see everything is "Faulted" meaning that an exception was thrown. Yet, still the application is proceeding merrily along the main thread

Finally, the enter key is pressed and the Task.WaitAll() method is called…. And the main thread only how has all those exceptions to contend with (in the form of an AggregateException)

Throw on WaitAll

That’s a bit of a gotcha if you don’t know where the AggregateException is coming from.

Parallelisation Talk Example – Aggregate Exceptions

The two code examples here show what happens when exceptions are thrown within tasks that are not handled within the task. In each case the task that has the error throws an exception.

In the first example, only one task throws an exception. Although from the output you can see that more tasks were expected to be launched the framework no longer schedules tasks to be started once an exception is thrown. Any existing tasks are continued to completion.

In the second example, all the tasks will throw exceptions. This is just to show that the aggregate exception is bringing back all the exceptions from the various tasks that were running.

Code Example 1 : Some bad

class Program
{
    static void Main(string[] args)
    {
        List<HotelRoomAvailability> hotelList = GetHotels();

        Console.WriteLine("These are the hotels to process");
        foreach(var hotel in hotelList)
            Console.WriteLine(hotel.HotelCode);

        Console.WriteLine(new string('=',79));


        try
        {
            Parallel.ForEach(hotelList, item => PopulateDetails(item));
        }
        catch (AggregateException aggex)
        {
            Console.WriteLine(aggex.Message);
            foreach(Exception ex in aggex.InnerExceptions)
                Console.WriteLine(ex.Message);
        }


        Console.WriteLine("Program finished");
        Console.ReadLine();

    }

    private static void PopulateDetails(HotelRoomAvailability hotel)
    {
        Console.WriteLine("Populating details of {0}", hotel.HotelCode);
        hotel.Name = HotelRespository.GetHotelName(hotel.HotelCode);
        hotel.Rates = AvailabilityRespository.GetRateInformation(
            hotel.HotelCode, hotel.StayDate, hotel.NumberOfNights);
    }

    private static List<HotelRoomAvailability> GetHotels()
    {
        List<HotelRoomAvailability> result = new List<HotelRoomAvailability>
            {
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONSOHO"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONLHRT4"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONLHRT5" // Not valid
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONWATERL" // Not valid
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONLHR123"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONCOVGDN"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONCTYAIR"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONLEISQR"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONPADDIN" // Not Valid
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONHIGHOL"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONKINGSX"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LONEUSTON"
                    }
            };

        return result;
    }
}

Output

The following is output first

These are the hotels to process
LONSOHO
LONLHRT4
LONLHRT5
LONWATERL
LONLHR123
LONCOVGDN
LONCTYAIR
LONLEISQR
LONPADDIN
LONHIGHOL
LONKINGSX
LONEUSTON
========================================================
Populating details of LONSOHO
Populating details of LONWATERL
Populating details of LONCTYAIR
Populating details of LONHIGHOL
Populating details of LONLHRT4

 

Aggregate Exception Example - Before

Then an exception is thrown

Aggregate Exception Example - Exception Assistant

And the final output looks like this:

These are the hotels to process
LONSOHO
LONLHRT4
LONLHRT5
LONWATERL
LONLHR123
LONCOVGDN
LONCTYAIR
LONLEISQR
LONPADDIN
LONHIGHOL
LONKINGSX
LONEUSTON
========================================================
Populating details of LONSOHO
Populating details of LONWATERL
Populating details of LONCTYAIR
Populating details of LONHIGHOL
Populating details of LONLHRT4
One or more errors occurred.
The hotel code 'LONWATERL' does not match a known hotel
Program finished

 

Aggregate Exception Example - After

Code Example 2 : All bad

This example replaces the GetHotels method, above, with a method that creates a list of entirely non-existant hotels:

class Program
{
    static void Main(string[] args)
    {
        List<HotelRoomAvailability> hotelList = GetHotels();

        Console.WriteLine("These are the hotels to process");
        foreach(var hotel in hotelList)
            Console.WriteLine(hotel.HotelCode);

        Console.WriteLine(new string('=',79));


        try
        {
            Parallel.ForEach(hotelList, item => PopulateDetails(item));
        }
        catch (AggregateException aggex)
        {
            Console.WriteLine(aggex.Message);
            foreach(Exception ex in aggex.InnerExceptions)
                Console.WriteLine(ex.Message);
        }


        Console.WriteLine("Program finished");
        Console.ReadLine();

    }

    private static void PopulateDetails(HotelRoomAvailability hotel)
    {
        Console.WriteLine("Populating details of {0}", hotel.HotelCode);
        hotel.Name = HotelRespository.GetHotelName(hotel.HotelCode);
        hotel.Rates = AvailabilityRespository.GetRateInformation(
            hotel.HotelCode, hotel.StayDate, hotel.NumberOfNights);
    }

    private static List<HotelRoomAvailability> GetHotels()
    {
        List<HotelRoomAvailability> result = new List<HotelRoomAvailability>
            {
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "BRISTOL"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "BIRMINGHAM"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "MANCHESTER" // Not valid
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LIVERPOOL" // Not valid
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "CARLISLE"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "CAMBRIDGE"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "OXFORD"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "READING"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "LEEDS" // Not Valid
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "NEWCASTLE"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "EDINBURGH"
                    },
                new HotelRoomAvailability
                    {
                        StayDate = new DateTime(2011, 7, 1),
                        NumberOfNights = 3,
                        HotelCode = "GLASGOW"
                    }
            };

        return result;
    }
}

Output

These are the hotels to process
BRISTOL
BIRMINGHAM
MANCHESTER
LIVERPOOL
CARLISLE
CAMBRIDGE
OXFORD
READING
LEEDS
NEWCASTLE
EDINBURGH
GLASGOW
============================================================
Populating details of BRISTOL
Populating details of LIVERPOOL
Populating details of OXFORD
Populating details of NEWCASTLE
Populating details of BIRMINGHAM
One or more errors occurred.
The hotel code 'LIVERPOOL' does not match a known hotel
The hotel code 'OXFORD' does not match a known hotel
The hotel code 'NEWCASTLE' does not match a known hotel
The hotel code 'BIRMINGHAM' does not match a known hotel
The hotel code 'BRISTOL' does not match a known hotel
Program finished

More Information

Parallelisation in .NET 4.0 – Part 2 Throwing Exceptions

With more threads running simultaneously in an application there is increasing complexity when it comes to debugging. When exceptions are thrown you usually catch them somewhere and handle them. But what happens if you throw an exception inside a thread?

Naturally, if you can handle the exception within the thread then that makes life much easier. But what if an exception bubbles up and out into code that created the thread?

In the example in my previous post on Parallelisation in .NET 4.0 had the calls to a third party service happening in separate threads. So, what happens if somewhere in the call an exception is raised.

In the service call GetAvailability, I’ve simulated some error conditions to throw exceptions based on the input to illustrate the examples. This is what it looks like:

public HotelAvail GetAvailability(string hotelCode, DateTime startDate, int nights)
{
    // Throw some exceptions depending on the input.
    if (hotelCode == null)
        throw new ArgumentNullException("hotelCode");

    if ((hotelCode.Length > 10) || (hotelCode.Length == 0))
        throw new ArgumentOutOfRangeException(
            "Hotel Codes are 1 to 10 chars in length. Got code which was " +
            hotelCode.Length + " chars.");

    if (hotelCode.StartsWith("Z"))
        throw new AvailabilityException("Hotel code '" + hotelCode +
                                        "' does not exist"); // A custom exception type
    // ... etc. ...
}

The calling code, from the previous example, looks like this:

public IEnumerable<HotelAvail> GetAvailability(IEnumerable<string> codes,
        DateTime startDate, int numNights)
{
        return codes.AsParallel().Select(code =>
            new AvailService().GetAvailability(code, startDate, numNights))
            .ToList();
}

If we provide incorrect input into the service such that it causes exceptions to be raised then Visual Studio responds in the normal way by breaking the debugging session at the point closest to where the exception is thrown.

If we were to wrap the call to the service in a try catch block (as in the following code sample) then we’d except that Visual Studio wouldn’t break the debugging session as there is a handler (the catch block) for the exception.

public IEnumerable<HotelAvail> GetAvailabilityPlinqException(IEnumerable<string> codes,
        DateTime startDate, int numNights)
{
    try
    {
        return codes.AsParallel().Select(code =>
            new AvailService().GetAvailability(code, startDate, numNights))
            .ToList();
    }
    catch (Exception ex)
    {
        // Do stuff to handle the exception.
    }
    return null;
}

Normally, that would be the case, however if the handler is outside the thread that threw the exception, as in the above example, the situation is somewhat different. In this case the Exception Assistant will appear and highlight the exception (or the code nearest the exception if it can’t highlight the throw statement itself*)

AvailabilityException in Exception Assistant

This happens because the exception is not caught within the thread in which it was originally thrown.

The AggregateException

If you just tell the debugger to continue executing the application it will continue, but the code that created the threads will have to handle an AggregateException. This is a special exception class that contains an InnerExceptions (note the plural) property that contains all the exceptions thrown from each of the threads.

AggregateException.InnerExceptions

You can enumerate over each of the inner exceptions to find out what happened in each of the threads.

Be aware, however, that an Aggregate exception can, itself, contain an AggregateException. So simply calling InnerExceptions may yet yield another AggregateException. For example if the hierarchy of exceptions looks like this:

AggregateException Hierarchy

Then the results of iterating over the InnerExceptions will be:

foreach(Exception ex in aggregateException.InnerExceptions)
{
    // ... do stuff ...
}
  • AggregateException
  • ApplicationException

You can flatten the hierarchy into a single AggregateException object that doesn’t contain InnerExceptions with any additional AggregateException objects. To do this call Flatten() on the original AggregateException. This returns a new AggregateException which you can then call InnerExceptions on and not have to worry about any hierarchy.

For example:

foreach(Exception ex in aggregateException.Flatten().InnerExceptions)
{
    // ... do stuff ...
}

Which results in the following exceptions being enumerated by the loop:

  • ApplicationException
  • NullReferenceException
  • ArgumentException
  • DivideByZeroException

But it’s broken, why doesn’t it just stop?

Well, it does. Once a thread has thrown an exception that bubbles up and out then no new tasks are started, so no new threads are created, and no new work gets done. However, remember that there will be other threads running as well and if one breaks, maybe others will break too, or maybe they will complete successfully. We won’t know unless they are allowed to finish what they are doing.

Going back to the room availability example if the input hotel codes contain invalid codes then it will throw an exception that is not caught within the thread. What if a selection of good and bad hotel codes are passed:

1, 2, 3, Z123, 4, 5, 6, 1234567890ABC, 7, 8, 9

Of the above list “Z123” and “1234567890ABC” are both invalid and produce different exceptions. However, when running tests the AggregateException only contains one of the exceptions.

To show what happens, I’ve modified my “service” like this and run it through a console applications. Here’s the full code:

The service class

public class AvailService
{
    // ...

    public HotelAvail GetAvailability(string hotelCode, DateTime startDate, int nights)
    {
        Console.WriteLine("Start @ {0:HH-mm-ss.fff}: {1}", DateTime.Now, hotelCode);

        ValidateInput(hotelCode);

        // ... do stuff to process the request ...

        Console.WriteLine("  End @ {0:HH-mm-ss.fff}: {1}", DateTime.Now, hotelCode);
        return result;
    }

    private void ValidateInput(string hotelCode)
    {
        if (hotelCode == null)
        {
            Console.WriteLine("Error @ {0:HH-mm-ss.fff}: hotelCode is null", DateTime.Now);
            throw new ArgumentNullException("hotelCode");
        }

        if ((hotelCode.Length > 10) || (hotelCode.Length == 0))
        {
            Console.WriteLine("Error @ {0:HH-mm-ss.fff}: hotelCode is {1}", DateTime.Now, hotelCode);
            throw new ArgumentOutOfRangeException(
                "Hotel Codes are 1 to 10 chars in length. Got code which was " +
                hotelCode.Length + " chars.");
        }

        if (hotelCode.StartsWith("Z"))
        {
            Console.WriteLine("Error @ {0:HH-mm-ss.fff}: hotelCode is {1}", DateTime.Now, hotelCode);
            throw new AvailabilityException("Hotel code '" + hotelCode +
                                            "' does not exist");
        }
    }
}

The method on the controller class

public IEnumerable<HotelAvail> GetAvailability(IEnumerable<string> codes,
        DateTime startDate, int numNights)
{
    return codes.AsParallel().Select(code =>
        new AvailService().GetAvailability(code, startDate, numNights))
        .ToList();
}

The Main method on the Program class

static void Main(string[] args)
{
    string[] codes = "1,2,3,Z123,4,5,6,1234567890ABC,,7,8,9".Split(',');
    AvailController ctrl = new AvailController();

    DateTime start = DateTime.Now;
    try
    {
        var result = ctrl.GetAvailability(codes,
            DateTime.Today.AddDays(7.0), 2);
    }
    catch (AggregateException aex)
    {
        Console.WriteLine(aex.Message);

        foreach (Exception ex in aex.InnerExceptions)
            Console.WriteLine(" -- {0}", ex.Message);

    }
    finally
    {
        DateTime end = DateTime.Now;
        Console.WriteLine("Total time in ms: {0}",
                            (end - start).TotalMilliseconds);

    }
}

And the console output is:

Start @ 16-36-36.518: 7
Start @ 16-36-36.518: Z123
Start @ 16-36-36.518: 6
Start @ 16-36-36.518: 1
Error @ 16-36-36.526: hotelCode is Z123
  End @ 16-36-42.438: 1
  End @ 16-36-42.654: 6
  End @ 16-36-42.900: 7
One or more errors occurred.
 -- Hotel code 'Z123' does not exist
Total time in ms: 6400

As you can see only 4 items got started out of an initial input collection of 11 items. The error occurred 8ms after these items started. Those items that did not cause an error were allowed to continue to completion. The result variable in the Main method will never have anything because of the exception so we never get the results of the three items that did succeed.

Naturally, the best course of action is not to let the exception bubble up and out of the thread in which the code is executing.

 

 

* Note, there appears to be a bug in Visual Studio with the Exception Assistant not always highlighting the correct line of code.

Follow

Get every new post delivered to your Inbox.