Saturday, August 31, 2013

How Parallel.ForEach Handles Exceptions

Think quick, what does this output:

var ints = Enumerable.Range(0, 99);
try
{
    Parallel.ForEach(ints, i =>
    {
        throw new Exception("Exception #" + i);
    });
}
catch (AggregateException ex)
{
    Console.WriteLine("Caught " + ex.Flatten().InnerExceptions.Count + " exceptions");

}

The Task Parallel Library's Parallel.ForEach statement can make things easier, but it may not always work the way you expect.

If you said either "Caught 100 exceptions" or "Caught 1 exceptions" you're wrong.

How Stuff Works

This one threw me recently (pardon the pun) and I had to look it up.  At first the documentation says:

Sometimes, two steps take place in the opposite order than they would if the loop were sequential. The only guarantee is that all of the loop's iterations will have run by the time the loop finishes.

Which might lead you to think it would execute all 100 iterations before throwing the Aggregate exception.  But that's a lie, because later it says:

If the body of a parallel loop throws an unhandled exception, the parallel loop no longer begins any new steps. By default, iterations that are executing at the time of the exception, other than the iteration that threw the exception, will complete. After they finish, the parallel loop will throw an exception in the context of the thread that invoked it.

In other words Parallel.ForEach takes the items you're enumerating, breaks them up into groups (whose size is based on the MaxDegreeOfParallelism property and the number of cores on your machine) and then simultaneously executes the lambda expression on all of the items in the first group, then all the items in the second group, etc.

If an exception occurs while processing of one of the items in a group then Parallel.ForEach will continue executing all of the items in the current group, but it will not execute any subsequent groups.

So what does the code above output?  The answer is "Caught [group size] exceptions".  On my machine with 8 logical processor that number happens to be 9.

Don't Stop Now

If you want to execute all iterations, regardless of exceptions, to ensure that as many side effects happen as possible while handling exceptions later en-mass you could try:

ints.AsParallel().ForAll(i => { DoStuff(i); });

But you'll quickly discover it has the same problem.  What you're probably looking for is something like Task.WaitAll:

var tasks = ints.Select(i => new TaskFactory().StartNew(() => DoStuff(i)));
Task.WaitAll(tasks.ToArray());

The above works by mapping your input array into a list of tasks via a .Select() statement.  Then, by default, Task.WaitAll will block until all tasks have run to completion, regardless of whether there was an exceptional state or not.

Conclusion

Exception handling and threading are some of the trickiest topics to master.  Hopefully, like me as of today, you're now slightly better prepared for writing more robust multi-threaded applications.

No comments: