In this chapter, we will cover the following recipes:
Creating a task
Waiting for tasks to finish
Returning results from a task
Passing data to a task
Creating a child task
Lazy task execution
Handling task exceptions using try/catch block
Handling task exceptions with AggregateException.Handle
Cancelling a task
Cancelling one of many tasks
At the beginning of the personal computer era, there was no concept of multiple threads offered by an operating system. Typically, operating system code and application code ran on a single thread of execution. The problem with this was that if a single application misbehaved, or simply took a long time to execute, the whole machine would stall, and often had to be rebooted.
As the development of the Windows operating systems progressed, Microsoft realized that they needed to improve this situation. In the Windows NT kernel, each application runs in its own process. A process is a collection of resources in which a virtual address space is allocated for each application. The advent of these processes ensured that code and data being used by one application could not be accessed and corrupted by another application, thus improving the reliability of the system.
Each process in Windows was also given its own thread. A thread is an operating system construct that functions like a virtual CPU. At any given moment, one of these threads is allowed to run on the physical CPU for a slice of time. When the time for a thread to run expires, it is swapped off of the CPU for another thread. Therefore, if a single thread enters an infinite loop, it can't monopolize all of the CPU time on the system. At the end of its time slice, it will be switched out for another thread.
Over the years, computers with multiple processors began to appear. These multiple processor machines were able to execute multiple threads at once. It became possible for an application to spawn new threads to run a compute-bound process asynchronously, thus gaining a performance improvement.
Over the past few years, the trend in processor development has shifted from making processors faster and faster, to making processors with multiple CPU cores on a single physical processor chip. Individuals who purchase these new machines expect their investment to pay off in terms of applications which are able to run efficiently across the available processor cores. Maximizing the utilization of the computing resources provided by the next generation of multi-core processors requires a change in the way the code is written.
The .NET framework has supported writing multi-threaded applications from the beginning, but the complexity of doing so has remained just out of reach for many .NET developers. To fully take the advantage of multi-threading, you needed to know quite a bit about how Windows works under the hood. For starters, you had to create and manage your own threads, which can be a demanding task as the number of threads in an application grows, and can often be the source of hard-to-find bugs.
Finally, help has arrived. Starting in .NET 4.0, Microsoft introduced the .NET Parallel Extensions, which gave us a new runtime, new class library types (the Task Parallel Library (TPL)), and new diagnostic tools to help with the inherent complexities of parallel programming.
The TPL isn't just a collection of new types. It's a completely new way of thinking about parallel programming. No longer do we need to think in terms of threads. With the TPL, we can now think in terms of task
. With this new task-based model, we just need to figure out the pieces of our application that can execute concurrently, and convert those pieces into tasks. The runtime will take care of managing and creating all of the underlying threads that actually do the work. The System.Threading.Task
class
in itself is just a wrapper for passing a delegate, which is a data structure that refers to a static
method or to a class instance, and an instance method of that class.
A TPL task
still uses the classic thread pool internally, but the heavy lifting of spinning up new threads to carry out the tasks
and determining the optimum number of threads required to take full advantage of the hardware, is all done by the runtime.
In this chapter, we will take a look at the basics of creating a parallel task
. You will learn how to pass data into a Task
using the Task
state object, returning data from a Task
, cancelling the Task
, and handling exceptions within a Task
.
Tasks
are an abstraction in the .NET framework to represent asynchronous units of work. In some ways, a task resembles the creation of a classic .NET thread, but provides a higher level of abstraction, which makes your code easier to write and read.
We will look at the three basic ways to create and run a new task.
In this recipe, we will create a new task using each of these three methods. To give our tasks something to do, we will be using WebClient
to read the text of three classic books. We will then split the words of each book into a string array, and display a count of the words in each book.
Ok, let's start building a Console
application that demonstrates the various ways to create a parallel task.
Launch Visual Studio 2012.
Start a new project using the C# Console Application project template, and assign
SimpleTasks
as the Solution name as shown in the following screenshot:Add the following
using
statements at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading.Tasks;
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
First, let's create a task using
Parallel.Invoke
. Add the following code to theMain
method of theProgram
class:char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"; Parallel.Invoke(() => { Console.WriteLine("Starting first task using Parallel.Invoke"); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words =client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Origin of Species word count: {0}", wordArray.Count()); client.Dispose(); } );
Next, let's start
task
using theStart
method of theTask
object. Add the following code to theMain
method of theProgram
class just below the code for the previous step:var secondTask = new Task(() => { Console.WriteLine("Starting second task using Task.Start"); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/16328/16328-8.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Beowulf word count: {0}", wordArray.Count()); client.Dispose(); } ); secondTask.Start();
Finally, let's create
task
usingTask.Factory.StartNew
. Add the following code to theMain
method of theProgram
class:Task.Factory.StartNew(() => { Console.WriteLine("Starting third task using Task.Factory.StartNew"); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/4300/4300.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Ulysses word count: {0}", wordArray.Count()); client.Dispose(); } ); //wait for Enter key to exit Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see output similar to the following screenshot. Note that the exact order of the text you see may vary as
tasks
run asynchronously:
The
Parallel.Invoke
method can implicitly create and run any number of statements concurrently by passing an action delegate for each delegate of work to be done.
Parallel.Invoke(( )=>DoSomething( ), ( )=>DoSomethingElse( ));
It is worth noting however, that the number of tasks
actually created by Parallel.Invoke
may or may not be equal to the number of delegates passed in, especially if there are a large number of delegates.
Using Task.Start()
or Task.Factory.StartNew()
creates new tasks explicitly. The new tasks
will be allocated threads by the ThreadPool
class, which handles the actual creation of the threads the tasks
use for carrying out their work. As developers, we are shielded from all of this thread creation work, because it is done for us by the Task
object.
When you create a task, you are really just creating a wrapper around a delegate of work to be performed. The delegate can be a named delegate and anonymous method, or a lambda expression.
So, which of these methods of creating task
is the best? Task.Factory.StartNew
is usually the preferred method, because it is more efficient in terms of the synchronization costs. Some amount of synchronization cost is incurred when using Thread.Start
, because it is necessary to ensure that another thread is not simultaneously calling start on the same Task
object. When using Task.Factory.StartNew
, we know that the task has already been scheduled by the time task
reference is handed back to our code.
Note also that you can't call Start()
on a task that has already run and completed. If you need the tasks to do the work again, you need to create new task
with the same delegate of work.
For the remainder of this book, we will primarily be using Task.Factory.StartNew
.
When developing a parallel application, you will often have situations where a task must be completed before the main thread can continue processing. The Task Parallel Library includes several methods that allow you to wait for one or more parallel tasks
to complete. This recipe will cover two such methods: Task.Wait()
and Task.WaitAll()
.
In this recipe we will be creating three tasks, all of which read in the text classic books and produce a word count
. After we create the first task, we will wait for it to complete using Task.Wait()
, before starting the second and third task. We will then wait for both the second and third tasks to complete using Task.WaitAll()
before writing a message to the console.
Let's create a Console
application that demonstrates how to wait for task
completion.
Launch Visual Studio 2012.
Start a new project using the C# Console Application project template, and assign
WordCount
as the Solution name.Add the following
using
statements at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading.Tasks;
In the
Main
method of theProgram
class, add a character array containing the basic punctuation marks. We will use this array instring.Split()
to eliminate punctuation marks. Also add a stringconstant
for theuser-agent
header of theWebClient
.char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)";
OK, now let's create our first task. This task will use
WebClient
to read the Origin of Species by Darwin, and get its word count. Enter the following code in theMain
method of theProgram
class just below the previous statement:var task1 = Task.Factory.StartNew(() => { Console.WriteLine("Starting first task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Origin of Species word count: {0}", wordArray.Count()); } );
Now, just below the previous task, write the following statements to wait on the task, and write a message to the
Console
application:task1.Wait(); Console.WriteLine("Task 1 complete. Creating Task 2 and Task 3.");
Below the previous statement, enter the code to create the second and third tasks. These tasks are very similar to the first task.
var task2 = Task.Factory.StartNew(() => { Console.WriteLine("Starting second task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/16328/16328-8.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Beowulf word count: {0}", wordArray.Count()); }); var task3 = Task.Factory.StartNew(() => { Console.WriteLine("Starting third task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/4300/4300.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Ulysses word count: {0}", wordArray.Count()); });
Finally, let's use
Task.WaitAll()
to wait for the second and third task to complete, then prompt the user to exit the program.Task.WaitAll()
takes an array oftask
as its parameter, and can be used to wait for any number of tasks to complete.Task.WaitAll(task2,task3); Console.WriteLine("All tasks complete."); Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see output similar to the following screenshot. Note that the exact order of the last few lines of text may still vary depending on the execution order of the second and third tasks.
Although Task.Wait()
and Task.WaitAll()
are fairly self-explanatory, both have several overloads that offer different functionalities.
Task.Wait()
can take either an Int32
or TimeSpan
parameter to specify a specific period of time to wait. It can also accept a CancellationToken
token parameter for cancellation, which will be covered later in the chapter.
Task.WaitAll()
always takes an array of Task
as its first parameter, and has a second parameter which can be an Int32
or TimeSpan
as in Task.Wait
.
Another useful method not shown in the recipe is Task.WaitAny()
. WaitAny
is very similar to WaitAll
, except that it waits for only one Task
in the array of Task
to complete. The first Task
of Task
array to finish, completes the wait condition, and execution of the main thread is allowed to move forward.
It is important to note that when you call one of the Wait
methods, the runtime will check to see if the task you are waiting on has started executing. If task
has started executing, then the thread that called Wait
will block until task
has finished executing. However, if task
has not started running, then the runtime may execute the task using the thread that calls Wait
.
The various overloads and behaviors of Task.Wait
, Task.WaitAll
, and Task.WaitAny
are shown in the following table:
So far, our tasks have not returned any values. However, it is often necessary to return a result from a task so it can be used in another part of our application. This functionality is provided by the Result
property of Task<TResult
>.
In this recipe, we will be creating a solution similar with tasks similar to the previous solution, but each of our three tasks return a result which can then be used to display the word count to the user.
Let's go to Visual Studio and see how we can return result values from our tasks.
Start a new project using the C# Console Application project template, and assign
WordCount2
as the Solution name.Add the following
using
statements are at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading.Tasks;
In the
Main
method of theProgram
class, add a character array containing the basic punctuation marks. We will use this array instring.Split()
to eliminate punctuation marks. Also add a string constant for theWebClient
user-agent
header.char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)";
Start by creating three tasks of type
Task<int>
namedtask1
,task2
, andtask3
. Your tasks should look as shown in the following code snippet:Task<int> task1 = Task.Factory.StartNew(() => { Console.WriteLine("Starting first task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); }); Task<int> task2 = Task.Factory.StartNew(() => { Console.WriteLine("Starting second task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/16328/16328-8.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); }); Task<int> task3 = Task.Factory.StartNew(() { Console.WriteLine("Starting third task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/4300/4300.txt"); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); });
Immediately below your tasks, add
Console.Writeline()
statements that useTask.Result
to display the results to the user. The remainder of theMain
method should now look as shown in the following code snippet:Console.WriteLine("task1 is complete. Origin of Species word count: {0}",task1.Result); Console.WriteLine("task2 is complete. Beowulf word count: {0}", task2.Result); Console.WriteLine("task3 is complete. Ulysses word count: {0}", task3.Result); Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see output similar to the following:
Task<TResult>
subclasses the standard Task
class and provides the additional feature of the ability to return a value. This is done by switching from providing an Action
delegate to providing a Func<TResult>
delegate.
It is worth noting that calling the Task.Result
accessor will ensure that the asynchronous operation is complete before returning, so this is another method of waiting for a task to complete. Once the result of Task
is available, it will be stored and returned immediately on later calls to the Result
accessor.
You can supply the data used by task
by passing an instance of System.Action<object>
and an object representing the data to be used by the action.
In this recipe, we will be revisiting our WordCount example, but this time we will be parameterizing the data the tasks will act upon.
The ability to pass data into a task allows us to create a single task that can operate on multiple pieces of input data. Let's create a Console
application so we can see how this works:
Start a new project using the C# Console Application project template and assign
WordCount3
as the Solution name.Add the following
using
statements at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Collections.Generic;
In the
Main
method of theProgram
class, add a character array containing the basic punctuation marks. We will use this array instring.Split()
to eliminate punctuation marks. Also add a constant string for theWebClients
user-agent task.char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)";
For this recipe, let's create a
new Dictionary
instance that can hold our book titles and URLs. Immediately after the previous statement, add the following code to create and initialize thedictionary
:var dictionary = new Dictionary<string, string> { {"Origin of Species", "http://www.gutenberg.org/files/2009/2009.txt"}, {"Beowulf", "http://www.gutenberg.org/files/16328/16328-8.txt"}, {"Ulysses", "http://www.gutenberg.org/files/4300/4300.txt"} };
This time we will be creating anonymous tasks in a loop. We still would like to wait for the tasks to complete before prompting the user to exit the program. We need a collection to hold our tasks, so we can pass them to
Task.WaitAll()
and wait for completion. Below the previous statement, create aList<Task>
to hold our tasks.var tasks = new List<Task>();
Next, we want to create a
for
loop to loop throughKeyValuePairs
in the dictionary. Let's put thefor
loop below the previous statement.foreach (var pair in dictionary) { }
Inside the body of your
for
loop, put the definition oftask
, and add it to your task list as follows. Note theKeyValuePair
being passed intotask
is in the form of an object. In the delegate body, we cast this object back to aKeyValuePair
. Other than that, task is pretty much the same.tasks.Add( Task.Factory.StartNew((stateObj) => { var taskData = (KeyValuePair<string, string>)stateObj; var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(taskData.Value); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); Console.WriteLine("Word count for {0}: {1}", taskData.Key, wordArray.Count()); },pair));
After the
for
loop, let's finish things up by waiting on the tasks to complete usingTask.WaitAll()
and prompting the user to exit. The last few lines should be as follows:Task.WaitAll(tasks.ToArray()); Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see output as shown in the following screenshot:
By passing data to Task
using the state feature, we now have a very powerful model for task creation, because we can create many tasks at once, each having the same code statements in the body and passing in the data that Task
operates on. It also makes our code much more concise and readable.
In our application we need to pass two items of data into the task
: a book title and the URL of the book, so we created dictionary
.
var dictionary = new Dictionary<string, string> { {"Origin of Species", "http://www.gutenberg.org/files/2009/2009.txt"}, {"Beowulf", "http://www.gutenberg.org/files/16328/16328-8.txt"}, {"Ulysses", "http://www.gutenberg.org/files/4300/4300.txt"} };
We would also want to wait on all of these tasks to complete before we prompt the user to exit, so we need to create a collection that can be converted to an array of tasks to hold our Task
objects. In this case, we made a list of tasks. In the body of our look that creates the tasks, we will add the tasks to the list.
var tasks = new List<Task>(); foreach (var pair in dictionary) { tasks.Add( //TASK DECLARATION HERE )); }
In our loop, we will pass in each of KeyValuePairs
in dictionary
as an object, using the Task(Action<Object>, Object)
constructor. This syntax is just a bit odd because you actually refer to the state
object twice.
Task.Factory.StartNew((stateObj) => { // TASK Body },pair ));}
The key takeaway here is that the only way to pass data to a Task
constructor is using Action<Object>
. To use the members of a specific type, you must convert or explicitly cast the data back to the desired type in the body of the Task
.
var taskData = (KeyValuePair<string, string>)stateObj;
Code that is running a task can create another task with the TaskCreationOptions.AttachedToParent set
. In this case, the new task becomes a child of the original or parent task.
In this recipe, we will be using a simplified version of the WordCount solution that uses a parent task to get the text of one book into a string array, and then spins up a child task to print the results.
Let's return to our WordCount solution, so we can see how to create a child task and attach it to a parent.
Start a new project using the C# Console Application project template, and assign
WordCount4
as the Solution name.Add the following
using
statements at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading.Tasks;
In the
Main
method of theProgram
class, add a character array containing the basic punctuation marks. We will use this array instring.Split()
to eliminate punctuation marks. Also, add a constant string for theWebClient
user-agent
header.char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)";
First, let's create the basic structure of our parent task. This is very similar to the other tasks we have created so far, and takes no parameters, and returns no values.
Task parent = Task.Factory.StartNew(() => { Console.WriteLine("Parent task starting"); const string uri = "http://www.gutenberg.org/files/2009/2009.txt"; var client = new WebClient(); client.Headers.Add("user-agent", headerText); var book = client.DownloadString(uri); var wordArray = book.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); // Child Task will go here });
Next, right after the comment in the parent task, let's create a child task to print the results and set the
AttachedToParent
option.Task.Factory.StartNew(()=> { Console.WriteLine("Child task starting"); Console.WriteLine("Word count for Origin of Species: {0}",wordArray.Count()); Console.WriteLine("Attached child task completed."); },TaskCreationOptions.AttachedToParent);
Finally, just below the close of the parent task, let's wait for the parent task to complete, and prompt the user to exit the application with the following code:
parent.Wait(); Console.WriteLine("Parent task completed."); Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
That's pretty much it. In Visual Studio 2012, press F5 to run the project. You should see output as shown in the following screenshot:
Using TaskCreationOptions.AttachedToParent
expresses structured parallelism. The parent task will wait for the child task to finish, so at the end of our program, all we have to do is wait for the parent task.
The nested child task, itself, is just an ordinary task
created in the delegate of another task
. A parent task may create any number of child tasks, limited only by system resources.
You can also create a nested task without using TaskCreationOptions.AttachedToParent
. The only real difference is that the nested tasks created without this option are essentially independent from the outer task. A task created with the TaskCreationOptions.AttachedToParent
option set is very closely synchronized with the parent.
The outer task could also use the DenyChildAttach
option to prevent other tasks from attaching as child tasks. However, the same outer task could still create an independent nested task.
Lazy initialization of an object means that object creation is deferred until the object is actually used by your program. If you have a parallel task that you want to execute only when the value returned from the task is actually needed, you can combine lazy task execution with the Task.Factory.StartNew
method.
In this recipe, we will return to our, by now familiar WordCount solution, to show you how to execute a parallel task and compute a word count for our book, only when we display the result to the console.
Let's create a console application that demonstrates how we can defer task creation until the result of the task is needed.
Start a new project using the C# Console Application project template, and assign
WordCount5
as the Solution name.Add the following
using
statements at the top of yourProgram
class.using System; using System.Linq; using System.Net; using System.Threading.Tasks;
The first step is to declare
System.Threading.Task<int>
for lazy initialization. In theMain
method of yourProgram
class, put aLazy
declaration as follows:var lazyCount = new Lazy<Task<int>>(()= { //Task declaration and body go here });
Inside the
Lazy
initialization declaration, place the code to create to task. The entire statement should now look as the following code snippet:Task<int>.Factory.StartNew(() => { Console.WriteLine("Executing the task."); char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"; const string uri = "http://www.gutenberg.org/files/2009/2009.txt"; var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(uri); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); }));
Now we just need to write the result to the
Console
. Just add the following code to the end of your program:Console.WriteLine("Calling the lazy variable"); Console.WriteLine("Origin of species word count: {0}",lazyCount.Value.Result ); Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
All done. In Visual Studio 2012, press F5 to run the project. You should see the output as shown in the following screenshot:
System.Lazy<T>
creates a thread safe Lazy initialization of an object. Lazy initialization is primarily used to improve performance and avoid computational overhead until necessary. You can pass a delegate (remember that System Threading Task is just a wrapper around a delegate) to the System.Lazy
constructor, and as we have done in this recipe, you can use a lambda expression to specify a factory
method for object creation. This keeps all of the initialization code in one place.
Lazy
initialization occurs the first time the System.Lazy<T>.Value
property is accessed.
Let's face it; sometimes things just go wrong with our code. Even with the simplified parallel programming model provided by the TPL, we still need to be able to handle our exceptions.
Tasks use System.AggregateException
to consolidate multiple failures into a single exception object. In this recipe, we will take a look at the simplest way to handle System.AggregateException
in our tasks
: the try
/catch
blocks.
The try-catch statement consists of a try block followed by one of more catch blocks, which specify handlers for different exceptions. The try block contains the guarded code that may cause the exception.
For this recipe we need to turn off the Visual Studio 2012 Exception Assistant. The Exception Assistant appears whenever a runtime exception is thrown, and intercepts the exception before it gets to our handler.
To turn off the Exception Assistant, go to the Debug menu and select Exceptions.
Uncheck the user-unhandled checkbox next to Common Language Runtime Exceptions.
Let's return to our WordCount solution so we can see how to handle an AggregateException
thrown by a parallel task.
Start a new project using the C# Console Application project template and assign
WordCount6
as the Solution name.Add the following
using
statements are at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading.Tasks;
For this recipe, we will just need a single task. The task will be very similar to our other word count tasks, but in this one we will simulate a problem with the
System.Net.WebClient
by creating and throwing aSystem.Net.WebException
. In theMain
method of yourProgram
class, createSystem.Task
that looks as the followingTask
:Task<int> task1 = Task.Factory.StartNew(() => { const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"; Console.WriteLine("Starting the task."); var client = new WebClient(); client.Headers.Add("user-agent", headerText); var words = client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); var ex = new WebException("Unable to download book contents"); throw ex; return 0; });
Just below the
Task
, let's put in ourtry
/catch
blocks as shown in the following code snippet. In thecatch
block, we will want to specifically catchSystem.AggregateException
.try { } catch (AggregateException aggEx) { }
Now let's implement the body of our
try
block. The body of thetry
block should be as shown in the following code snippet. There are a couple of subtle but important concepts in here that will be explained later in the chapter.try { task1.Wait(); if (!task1.IsFaulted) { Console.WriteLine("Task complete. Origin of Species word count: {0}",task1.Result); } }
Next, let's implement the body of our
catch
block. It should look as shown in the following code snippet:catch (AggregateException aggEx) { foreach (var ex in aggEx.InnerExceptions) { Console.WriteLine("Caught exception: {0}", ex.Message); } }
After the
catch
block, let's finish up by prompting the user to exit, and waiting on the user to hit Enter.Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see output as shown in the following screenshot:
All of this stuff has been pretty self-explanatory so far, but handling exceptions in task involves a couple of subtleties that need to be pointed out.
The task itself is pretty straightforward. Other than throwing the System.Net.WebException
, there is nothing out of the ordinary here.
Let's take a closer look at the try/catch blocks. The first statement in the try
block System.Threading.Task.Wait()
to wait on task completion. However, there is another purpose here. Unhandled exceptions thrown inside a task
are swallowed by the runtime and wrapped up in System.AggregateException
. It is your job to handle this.
The TPL also has the concept of AggregateException
being observed. If AggregateException
is raised by your task, it will only be handled if it is currently being observed. This is very important to understand. If you never take an action that causes the exceptions to be observed, you are going to have a problem. When the Task
object is garbage collected, the Finalize
method of the task
will see that the task
had unobserved exceptions, and it will throwSystem.AggregateException
. You will not be able to catch an exception thrown by the finalizer thread and your process will be terminated.
So how to you observe an AggregateException
, you ask? The Systm.Threading.Task
class has a few methods and properties call triggers that cause System.AggregateException
to be observed. A few of these are as follows:
Task.Wait
Task.WaitAny
Task.WaitAll
Task.Result
Using any of these trigger
methods indicates to the runtime that you are interested in observing any System.AggregateException
that occurs. If you do not use one of the trigger
methods on the Task
class, the TPL will not raise any AggregateException
, and an unhandled exception will occur.
Now, let's take a look at the catch
block. System.AggregateException
can wrap many individual exception objects. In our catch
block, we need to loop through AggregateException.InnerExceptions
to take a look at all of the individual exceptions that occurred in a task.
It is important to note that there is really no way to correlate an exception from the AggregateExcetion.InnerExceptions
collection back to the particular task
that threw an exception. All you really know is that some operation threw an Exception
.
System.AggregateException
overrides the GetBaseException
method of exception, and returns the innermost exception, which is the initial cause of the problem.
In this recipe, we will look at another way to handle System.AggregateException
, by using the AggregateException.Handle
method. The Handler
method invokes a handler function for each exception wrapped in AggregateException
.
For this recipe, we need to turn off the Visual Studio 2012 Exception Assistant. The Exception Assistant appears whenever a runtime exception is thrown and intercepts the exception before it gets to our handler.
To turn off the Exception Assistant, go to the Debug menu and select Exceptions.
Uncheck the user-unhandled checkbox next to Common Language Runtime Exceptions.
Let's take a look at how we can use AggregateException.Handle
to provide an alternate method to handling exceptions in a parallel application.
For this recipe, we will return to our WordCount6 project, and modify it to handle our exceptions in a different way. Start Visual Studio 2012 and open the WordCount6 project.
The first step is to define our handler function that will be invoked when we call
AggregateException.Handle
. Following theMain
method of yourProgram
class, add a newprivate static handler
method that returns a bool. It should look as the following code snippet:private static bool HandleWebExceptions(Exception ex) { if (ex is WebException) { Console.WriteLine(("Caught WebException: {0}", ex.Message); return true; } else { Console.WriteLine("Caught exception: {0}", ex.Message); return false; } }
The only other step here is to replace the body of your
catch
block with a call toSystem.AggregateException.Handle
, passing in theHandleWebExceptions
predicate. The updatedtry
/catch
block should look as follows:try { task1.Wait(); if (!task1.IsFaulted) { Console.WriteLine("Task complete. Origin of Species word count: {0}",task1.Result); } } catch (AggregateException aggEx) { aggEx.Handle(HandleWebExceptions); }
Those are the only modifications necessary. In Visual Studio 2012, press F5 to run the project. You should see output as shown in the following screenshot:
AggregateException.Handle()
takes a predicate that you supply, and the predicate will be invoked once for every exception wrapped in System.AggregateException
.
The predicate itself just needs to contain the logic to handle the various exception types that you expect, and to return true or false to indicate whether the exception was handled.
If any of the exceptions went unhandled, they will be wrapped in a new System.AggregateException
and thrown.
Up to this point, we have focused on creating, running, and handling exceptions in tasks
. Now we will begin to take a look at using System.Threading.CancellationTokenSource
and System.Threading.CancellationToken
to cancel tasks
.
This recipe will show how to cancel a single task.
Let's create a console application that shows how to cancel a parallel task.
Start a new project using the C# Console Application project template, and assign
WordCount7
as the Solution name.Add the following
using
statements at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks;
Let's start by creating
CancellationTokenSource
and getting ourCancellationToken
. In theMain
method of yourProgram
class, add the following statements://Create a cancellation token source CancellationTokenSource tokenSource = new CancellationTokenSource(); //get the cancellation token CancellationToken token = tokenSource.Token;
Now we need to create our
Task
and passCancellationToken
into the constructor. Right after the previous line, put in the followingTask
definition:Task<int> task1 = Task.Factory.StartNew(() => { // The body of the task goes here }, token);
In the body of our
Task
, we need to check theIsCancellationRequested
property ofCancellationToken
. If it has been, we dispose of our resource and throwOperationCancelledException
. If not, we do our usual work. Enter the following code into the body ofTask
://wait a bit for the cancellation Thread.Sleep(2000); const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"; var client = new WebClient(); client.Headers.Add("user-agent", headerText); if(token.IsCancellationRequested) { client.Dispose(); throw new OperationCanceledException(token); } else { var book = client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; Console.WriteLine("Starting the task."); var wordArray = book.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); }
Right after the task, put in the following lines to write the cancellation status to the
Console
, and then call theCancel
method ofTokenSource
.Console.WriteLine("Has the task been cancelled?: {0}", task1.IsCanceled); //Cancel the token source tokenSource.Cancel();
The following are the last statements put in a condition to check whether the task has been cancelled or faulted before we try to write out the results:
if (!task1.IsCanceled || !task1.IsFaulted) { try { if (!task1.IsFaulted) { Console.WriteLine("Origin of Specied word count: {0}", task1.Result); } } catch (AggregateException aggEx) { foreach (Exception ex in aggEx.InnerExceptions) { Console.WriteLine("Caught exception: {0}", ex.Message); } } } else { Console.WriteLine("The task has been cancelled"); }
Lastly, we'll finish up by prompting the user to exit and waiting for the input.
Console.WriteLine("Press <Enter> to exit."); Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see the output as shown in the following screenshot:
The basic idea of cancelling task
is that we create CancellationTokenSource
, obtain CancellationToken
from it, and then pass CancellationToken
onto the Task
constructor. Once we have done that, we can call the Cancel
method on the CancellationTokenSource
to cancel the task.
That's all easy enough. However, inside task
we have a couple of options on how to handle the cancellation.
If your task has resources that need to be cleaned up (such as the WebClient
), you need to check the cancellation tokens IsCancellationRequested
property, then dispose of the resources, and throw a new OperationCancelledException
.
The other option, if your task doesn't use resources which need to be explicitly cleaned up, is to use the token ThrowIsCancellationRequested()
, which will ensure the task transitions to a status of cancelled in a single statement.
If you need to execute task
and prevent it from being cancelled, you can obtain a special CancellationToken
that is not associated with any CancellationTokenSource
from the static CancellationToken.None
property, and pass this token to Task
. Since there is no associated CancellationTokenSource
, it is not possible to call Cancel
for this token, and any code that is checking the CancellationToken.IsCancellationRequested
property will always get back a false.
You can register one or more methods to be called when CancellationTokenSource
is cancelled, by registering a callback
method with the CancellationToken.Register
method. You just need to pass Action<Object>
, and optionally, a state value will be passed to callback
and a Boolean
, indicating whether to invoke the delegate using SynchronizationContext
of the calling thread to the Register
method. Passing in a value of false means the thread that calls Cancel
will invoke the registered methods synchronously. If you pass true, the callbacks will be sent to SynchronizationContext
, which determines which thread will invoke the callbacks.
var source = new CancellationTokenSource(); source.Token.Register(()=> { Console.WriteLine("The operation has been cancelled."); });
The Register
method of CancellationToken
returns a CancellationTokenRegistration
object. To remove a registered callback from CancellationTokenSource
, so it doesn't get invoked, call the Dispose
method of the CancellationTokenRegistration
object.
You can also create CancellationTokenSource
by linking other CancellationTokenSource
objects together. The new composite CancellationTokenSource
will be cancelled, if any of the linked CancellationTokenSource
objects are cancelled.
var source1 = new CancellationTokenSource(); var source2 = new CancellationTokenSource(); var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
Now that we have seen how to cancel a task, let's take a look at how we can use CancellationToken
to cancel multiple tasks with a single call to CancellationTokenSource.Cancel()
.
Now let's return to our WordCount example and create a Console
application that provides for the cancellation of multiple tasks with single CancellationToken
.
Start a new project using the C# Console Application project template, and assign
WordCount8
as the Solution name.Add the following
using
statements at the top of yourProgram
class:using System; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks;
First, let's create a
helper
method to display errors and cancellation status. Since we have multiple tasks, it's better to have this logic all in one place. Following theMain
method of yourProgram
class, create astatic
method callHandleExceptions
which will display the errors and task status to the user.private static void DisplayException(Task task, AggregateException outerEx, string bookName) { foreach (Exception innerEx in outerEx.InnerExceptions) { Console.WriteLine("Handled exception for {0}:{1}",bookName,innerEx.Message); } Console.WriteLine("Cancellation status for book {0}: {1}", bookName, task.IsCanceled); }
Next, at the top of the
Main
method, createCancellationTokenSource
and get ourCancellationToken
.//Create a cancellation token source CancellationTokenSource tokenSource = new CancellationTokenSource(); //get the cancellation token CancellationToken token = tokenSource.Token;
Now we need to create our tasks and our array of
delimiters
. The tasks are the same as in the recipe for cancelling a single task. The key here is that we are passing the sameCancellationToken
in for all three tasks.char[] delimiters = { ' ', ',', '.', ';', ':', '-', '_', '/', '\u000A' }; const string headerText = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT
6.1; Trident/6.0)";
Task<int> task1 = Task.Factory.StartNew(() => { // wait for the cancellation to happen Thread.Sleep(2000); var client = new WebClient(); client.Headers.Add("user-agent", headerText); if (token.IsCancellationRequested) { client.Dispose(); throw new OperationCanceledException(token); } else { var words = client.DownloadString(@"http://www.gutenberg.org/files/2009/2009.txt"); Console.WriteLine("Starting the task for Origin of Species."); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); } },token); Task<int> task2 = Task.Factory.StartNew(() => { // wait for the cancellation to happen Thread.Sleep(2000); var client = new WebClient(); client.Headers.Add("user-agent", headerText); if (token.IsCancellationRequested) { client.Dispose(); throw new OperationCanceledException(token); } else { var words = client.DownloadString(@"http://www.gutenberg.org/files/16328/16328-8.txt"); Console.WriteLine("Starting the task for Beowulf."); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); }; },token); Task<int> task3 = Task.Factory.StartNew(() => { // wait for the cancellation to happen Thread.Sleep(2000); var client = new WebClient(); client.Headers.Add("user-agent", headerText); if (token.IsCancellationRequested) { client.Dispose(); throw new OperationCanceledException(token); } else { var words = client.DownloadString(@"http://www.gutenberg.org/files/4300/4300.txt"); Console.WriteLine("Starting the task for Ulysses."); var wordArray = words.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); return wordArray.Count(); }; },token);OK, let's finish up by calling
CancellationTokenSource.Cancel()
, checking the results, and catching the exceptions. The remainder of theMain
method should look as the following code snippet://Cancel the token source tokenSource.Cancel(); try { if (!task1.IsFaulted || !task1.IsCanceled) { Console.WriteLine("Origin of Specied word count: {0}", task1.Result); } } catch(AggregateException outerEx1) { DisplayException(task1, outerEx1, "Origin of Species"); } try { if (!task2.IsFaulted || !task2.IsCanceled) { Console.WriteLine("Beowulf word count: {0}", task2.Result); } } catch (AggregateException outerEx2) { DisplayException(task2, outerEx2, "Beowulf"); } try { if (!task3.IsFaulted || !task3.IsCanceled) { Console.WriteLine("Ulysses word count: {0}", task3.Result); } } catch (AggregateException outerEx3) { DisplayException(task3, outerEx3, "Ulysses"); } Console.ReadLine();
In Visual Studio 2012, press F5 to run the project. You should see output as shown in the following screenshot:
Functionally, cancelling multiple tasks is the same as cancelling a single task. In fact, the Parallel Extensions team has put a lot of work into making cancellation of various parallel structures very similar, as you will see as we go through the book.
All that is necessary to cancel multiple tasks is to create CancellationToken
, then pass that token into all of the tasks you wish to cancel as shown in the following code snippet:
CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; Task<int> task1 = Task.Factory.StartNew(() => { ... },token); Task<int> task, = Task.Factory.StartNew(() => { ... },token); tokenSource.Cancel();