In the previous instalment of this tutorial we looked at the Parallel.Invoke method. This allows you to implicitly generate a set of tasks that may be executed in parallel. This is a useful method but affords little say over the execution of the individual tasks and the order in which they run. When you need more control over parallel tasks, you can use the Task class. This allows you to explicitly generate parallel tasks. The code needed for explicit task creation is slightly more complex than that for Parallel.Invoke but the benefits outweigh this disadvantage.
The Task class can perform the same functions as Parallel.Invoke. In addition, the following types of task can be created:
- Continuation Tasks. A continuation task replaces the use of callbacks that might be required when using other parallelism and multithreading techniques. This type of task is configured to start only after another task, or group of tasks, has completed. They can safely use data generated by those earlier tasks.
- Nested and Child Tasks. Nested tasks are simply tasks that are created within the scope of another task but remain independent. Child tasks are similar as they are created within a parent task. However, they are more tightly linked to that parent.
- Tasks with Return Values. When using explicit tasks, it is possible for a task to execute in parallel and yet still return a value that may be used once the task finishes.
In this article we will look at the basic form of task and how tasks may be executed in parallel. The other types of task will be considered over the next instalments in the tutorial.
The examples in this article can be executed in a console application. As the Task class is found in the System.Threading.Tasks namespace and we will also be using the Thread class from the System.Threading namespace, you should add the following using directives before executing the samples.
Running Parallel Tasks
The Task class provides a wrapper for an Action delegate. The delegate describes the code that you wish to execute and the wrapper provides parallelism and enables the features mentioned above. A simple way to create a task is to use the constructor that has a single parameter, which accepts the delegate that you wish to execute. In many cases, this delegate is defined as a lambda expression. Tasks do not execute immediately after being created. To start a task you call its Start method.
The following code simulates the steps that may be executed on launching a customer relationship management (CRM) application. Initially a message is displayed indicating that the application is starting. Next, two tasks are defined. The first simulates loading user data from a database. The second simulates retrieving customer data. The tasks are started by the two calls to the Start method before a message is displayed indicating that the CRM application is loaded.
Console.WriteLine("CRM Application Starting");
Task loadUserDataTask = new Task(() =>
Console.WriteLine("Loading User Data");
Console.WriteLine("User data loaded");
Task loadCustomerDataTask = new Task(() =>
Console.WriteLine("Loading Customer Data");
Console.WriteLine("Customer data loaded");
Console.WriteLine("CRM Application Loaded");
CRM Application Starting
CRM Application Loaded
Loading User Data
Loading Customer Data
User data loaded
Customer data loaded
The above code shows a common pattern for both parallel and multithreaded applications. The code would be running on the user interface (UI) thread, which can block input from the user and give the impression that the program has stopped responding. By starting the data retrieval tasks on separate threads we allow the program to become responsive before the information has been obtained. This gives a better user experience as long as we do not allow functions that rely upon the data to start before the data is present. You can see from the output that the application appears to be loaded and ready for use whilst the data access continues in the background.
You can also see from the output in the comments that the parallel tasks happened to start after the "CRM Application Loaded" message was displayed. It's important to understand that when you call the Start method the task might start immediately. However, if all processor cores are busy, the task may be delayed until it is efficient to execute it. As with other elements of the Task Parallel Library, Tasks provide potential parallelism, rather than guaranteed parallelism.
NB: If you press Enter to skip the Console.ReadLine call before the tasks complete you will encounter an exception. This is caused by the call to Dispose, as Task objects may not be disposed whilst still running.
22 September 2011