The Task Parallel Library Sampler – Part 9: Basic Exception handling with the AggregateException

Previous Post in this series:
Part 8: Adding a New Sample, Matrices Multiplication

In the updated solution you’ll find two new models, AggregateExceptionNoCatchSample and AggregateExceptionCatchSample. The TPL provides a convienient exception handling mechanism in the form of an AggregateException.

If you run through a Parallel.For or .ForEach and an exception is thrown in one of the threads, no more threads are created and any exceptions that are thrown across all threads are wrapped in an Aggregate exception and that is thrown upon leaving the Parallel.For or .ForEach.

The first sample here, AggregateExceptionNoCatchSample, throws an exception when getting to row 100. Of course remember that we’re spinning off threads and the row we’re looking at could be random. The code could run all rows up to 100 and than get thrown or get could get row 400 after only processing row 8.

AggregateExceptionNoCatchSample:

public class AggregateExceptionNoCatchSample : Sample
{
	//used by DrawGreyScale to set the number of rows we started
	private int rowsStarted = 0;

	public override string SampleName
	{
		get { return "Aggregate Exception - Don't Catch Exceptions Sample"; }
	}

	public override bool ImageRequired
	{
		get { return true; }
	}

	public override void Run(System.Drawing.Bitmap bmp = null, Action<string> UpdateLog = null)
	{
		Stopwatch s = new Stopwatch();
		s.Start();

		try
		{
			rowsStarted = 0;
			DrawGreyScale(bmp, UpdateLog);
		}
		catch (AggregateException ae)
		{
			UpdateLog("Started " + rowsStarted + " rows.");
			//if an exception is handled and true is returned then nothing happens
			//if an exception isn't handled and false is returned then all unhandled exceptions are rewrapped in
			//a new AggregateException and thrown again.
			ae.Handle((x) =>
			{
				if (x is StackOverflowException) // This we know how to handle.
				{
					UpdateLog("Handling a stack overflow exception.");
					return true;
				}
				else
				{
					UpdateLog("Unhandled exception.");
				}
				return false; // Let anything else stop the application.
			});
		}

		s.Stop();
		RunTime = s.Elapsed;
	}

	private void DrawGreyScale(System.Drawing.Bitmap bmp, Action<string> UpdateLog)
	{
		System.Drawing.Imaging.BitmapData bmData = bmp.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
		int stride = bmData.Stride;
		System.IntPtr Scan0 = bmData.Scan0;
		unsafe
		{
			byte* start = (byte*)(void*)Scan0;

			int height = bmp.Height;
			int width = bmp.Width;

			Parallel.For(0, height, y =>
			{
				UpdateLog("Starting line " + y.ToString());
				Interlocked.Increment(ref rowsStarted);
				byte* p = start + (y * stride);
				for (int x = 0; x < width; ++x)
				{
					byte blue = p[0];
					byte green = p[1];
					byte red = p[2];

					p[0] = p[1] = p[2] = (byte)(.299 * red
						+ .587 * green
						+ .114 * blue);

					p += 3;
				}

				if (y >= 100)
				{
					UpdateLog("Throwing an exception at " + y);
					if (y % 2 == 0)
						throw new StackOverflowException("yeah, we got a stack overflow.");
					else
						throw new ArgumentNullException("yeah, we got a null argument.");
				}
			});
		}
		bmp.UnlockBits(bmData);
	}
}

As mentioned an exception is thrown in DrawGreyScale upon getting to row 100. In all likelihood multiple rows would have been spun off for rows greater then or equal to 100. All of these rows that throw an exception will get combined into an AggregateException. Inside of the AggregateException is a property titled “InnerExceptions” that contains all of these exceptions. For even rows a StackOverflowException is thrown (for no reason other then I wanted to throw that type) and for odd rows an ArgumentNullException is throws (for the same reason as the StackOverflowException).

In the .Run of AggregateExceptionNoCatchSample the call to .DrawGreyScale is wrapped in a try/catch for an AggregateException. Reading the documentation on ExceptionHandling in the TPL it is recommended you don’t wrap a call like this in a try/catch and not do anything with the exceptions. Uh, yeah, well, I hope you wouldn’t catch exceptions and not do something with them.

In the catch .Handle is being called that invokes the predicate passed in. This will iterate through all exceptions in the InnerExceptions of the AggregateException so you can handle each one individually. If you handle the exception the predicate should return a true so that AggregateException knows not to do anything with it. If you don’t handle the exception you should return false. If any exceptions aren’t handled they are wrapped in a new AggregateException and thrown outside of the handle method. This way if you get multiple types of exceptions and one appears you weren’t expecting it can be handled higher up in the stack.

In the sample it may actually kill the app because I only handle the StackOverflowException and not the ArgumentNullException which will get re-thrown.

AggregateExceptionCatchSample:

public class AggregateExceptionCatchSample : Sample
{
	//used by DrawGreyScale to set the number of rows we started
	private int rowsStarted = 0;

	public override string SampleName
	{
		get { return "Aggregate Exception - Catch Exceptions Sample"; }
	}

	public override bool ImageRequired
	{
		get { return true; }
	}

	public override void Run(System.Drawing.Bitmap bmp = null, Action<string> UpdateLog = null)
	{
		Stopwatch s = new Stopwatch();
		s.Start();

		try
		{
			rowsStarted = 0;
			DrawGreyScale(bmp, UpdateLog);
		}
		catch (AggregateException ae)
		{
			UpdateLog("Started " + rowsStarted + " rows.");

			ae.Handle((x) =>
			{
				UpdateLog("Handling an exception.");
				return true;
			});
		}

		s.Stop();
		RunTime = s.Elapsed;
	}

	private void DrawGreyScale(System.Drawing.Bitmap bmp, Action<string> UpdateLog)
	{
		ConcurrentQueue<Exception> exceptions = new ConcurrentQueue<Exception>();
		System.Drawing.Imaging.BitmapData bmData = bmp.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
		int stride = bmData.Stride;
		System.IntPtr Scan0 = bmData.Scan0;
		unsafe
		{
			byte* start = (byte*)(void*)Scan0;

			int height = bmp.Height;
			int width = bmp.Width;

			Parallel.For(0, height, y =>
			{
				try
				{
					UpdateLog("Starting line " + y.ToString());
					Interlocked.Increment(ref rowsStarted);
					byte* p = start + (y * stride);
					for (int x = 0; x < width; ++x)
					{
						byte blue = p[0];
						byte green = p[1];
						byte red = p[2];

						p[0] = p[1] = p[2] = (byte)(.299 * red
							+ .587 * green
							+ .114 * blue);

						p += 3;
					}

					if (y >= 100)
					{
						UpdateLog("Throwing an exception at " + y);
						if (y % 2 == 0)
							throw new StackOverflowException("yeah, we got a stack overflow.");
						else
							throw new ArgumentNullException("yeah, we got a null argument.");
					}
				}
				catch (StackOverflowException)
				{
					UpdateLog("Internally handled the StackOverflowException.");
				}
				catch (Exception e)
				{
					exceptions.Enqueue(e);
				}
			});
		}
		bmp.UnlockBits(bmData);

		if (exceptions.Count > 0)
			throw new AggregateException(exceptions);
	}
}

In the AggregateExceptionCatchSample the exceptions are caught in the Parallel.For and dropped into a ConcurrentQueue if we don’t handle it. Then upon exiting the Parallel.For, if any exceptions were queued we throw a new AggregateException passing in the queue so it can get handled above us.

So why do this? The biggest advantage is that you may be able to handle the exceptions right in the thread with no reason to kill all threads. This way, if there are any threads that have exceptions you can’t handle then let them bubble up in the AggregateException. In this sample we assume we can handle all exceptions in the .Run and just return true in the .Handle of the catch so we don’t kill the app for a sample but you will only want to return true if you handle the exception and false if you don’t. Up next is stopping and breaking in a Parallel.For and .ForEach.

Thanks,
Brian

Leave a Reply