Welcome to the second part of the Parallel Programming series that consists of the following part:
In our preceding article and the first part of the entire Parallel Programming series we have learned the key basics of the TPL library and discovered how easy it is to create new tasks that will do the work concurrently. Especially when compared to the old .Net threading model, working with tasks is straightforward and much simpler since the underlying engine takes care of many key aspects for us, so you can easily take advantage of multi-core systems thus make the application faster and more responsive.
In this part, we will take a closer look into two topics that are a little bit advanced, both very important for mastering the TPL. First, we will learn about the options we have for waiting for tasks to complete. Next, we will dive deeper into exception handling that is crucial while working with threads since the application can give various unexpected results when not handled correctly.
Waiting For Tasks
In the previous part of the series, we have used the Task.Result method that stops the execution flow until the given task has completed. However, there are other three methods that allow us to wait for a task that doesn't return any result or for a set of tasks that is useful for achieving some coordination among them.
The following is a brief summary of these additional methods:
- The Call() method on the Task instance is used to wait until the task has completed. You can optionally set a maximum waiting duration or CancellationToken to enable the task cancellation.
- The staticTask.WaitForAll() method waits for all the tasks in the supplied task array to complete. Yet again you can add the maximum duration and the cancellation support.
- The static Task.WaitForAny() waits for the first task of a set of tasks to complete. Setting the duration and cancellation support are optional via the method arguments.
Waiting For A Single Task To Complete
By calling the instance Wait() method you can wait until a single task has completed. Note that the task is considered to be completed not only when it executes all its workload, but also when it has been cancelled or it has thrown an exception.
There are several overloaded methods available that allow you to add an instance of a CancellationToken to enable the task's cancellation or add some specific waiting time duration in the form of a number of milliseconds or a TimeSpan.
The following example demonstrates the use of the Wait() method. We start by creating a static Workload method that represents the workload. In this case, we will just print the iteration turn to the console and put the task to sleep for 1 second.
- static void Workload()
- {
- for (int i = 0; i < 5; i++)
- {
- Console.WriteLine("Task - iteration {0}", i);
-
-
- Thread.Sleep(1000);
- }
- }
We will now create two simple tasks and use Wait() to wait until they have completed. Note that we have used an overloaded version of the method with the second task and restricted it to wait only 2000 milliseconds (2 seconds).
- static void Main(string[] args)
- {
-
- Task task = new Task(new Action(Workload));
- task.Start();
-
-
- Console.WriteLine("Waiting for task to complete.");
- task.Wait();
- Console.WriteLine("Task Completed.");
-
-
- task = new Task(new Action(Workload));
- task.Start();
- Console.WriteLine("Waiting 2 secs for task to complete.");
- task.Wait(2000);
- Console.WriteLine("Wait ended - task completed.");
-
- Console.WriteLine("Main method complete. Press any key to finish.");
- Console.ReadKey();
- }
Image 1: Waiting for a single task to complete
Waiting For Several Tasks To Complete
The static Task.WaitAll() method is used to wait for a number of tasks to complete, so it will not return until all the given tasks will either complete, throw an exception or be cancelled. This method uses the same overloading pattern as the Wait() method.
For the sake of demonstration, we have created two tasks and will wait for both until they complete. Note that the second task will print just one line to the console, so it will finish almost immediately. Still, the WaitAll() method won't return until they both have completed.
- static void Main(string[] args)
- {
-
- Task task1 = new Task(() =>
- {
- for (int i = 0; i < 5; i++)
- {
- Console.WriteLine("Task 1 - iteration {0}", i);
-
-
- Thread.Sleep(1000);
- }
- Console.WriteLine("Task 1 complete");
- });
-
- Task task2 = new Task(() =>
- {
- Console.WriteLine("Task 2 complete");
- });
-
-
- task1.Start();
- task2.Start();
-
-
- Console.WriteLine("Waiting for tasks to complete.");
- Task.WaitAll(task1, task2);
- Console.WriteLine("Tasks Completed.");
-
- Console.WriteLine("Main method complete. Press any key to finish.");
- Console.ReadKey();
- }
Image 2: Waiting for several tasks to complete
Waiting For One Of Many Tasks To Complete
The static Task.WaitAny() method is very similar to the method above (WaitAll), but instead of waiting for all the tasks to complete, it waits only for the first one that either has completed, was cancelled or has thrown an exception. Moreover, it returns the array index of the first completed task.
In the following example, we are starting two tasks and waiting for the first one to finish. Because the workload of the second task is quite simple, it completes first. As a result, the WaitAny() method returns 1 since that is the array index of the first completed task.
- static void Main(string[] args)
- {
-
- Task task1 = new Task(() =>
- {
- for (int i = 0; i < 5; i++)
- {
- Console.WriteLine("Task 1 - iteration {0}", i);
-
- Thread.Sleep(1000);
- }
- Console.WriteLine("Task 1 complete");
- });
-
- Task task2 = new Task(() =>
- {
- Console.WriteLine("Task 2 complete");
- });
-
-
- task1.Start();
- task2.Start();
-
-
- Console.WriteLine("Waiting for tasks to complete.");
- int taskIndex = Task.WaitAny(task1, task2);
- Console.WriteLine("Task Completed - array index: {0}", taskIndex);
-
- Console.WriteLine("Main method complete. Press any key to finish.");
- Console.ReadKey();
- }
Image 3: Waiting for one of many tasks to complete
Note: When the WaitAny() method returns, all the other started tasks will continue executing.
Exceptions Handling In Tasks
No matter whether you write just a sequential application or add in some concurrency, you still must handle exceptions. Otherwise, the application will crash when an exception is thrown, leaving a poor use experience behind. In parallel programming, it is even more important since the application could behave unpredictably. Thankfully, TPL provides a consistent model for handling exceptions that are thrown during a task's execution.
When an exception occurs within a task (and is not caught), it is not thrown immediately. Instead, it is squirreled away by the .Net framework and thrown when some trigger member method is called, such as Task.Result, Task.Wait(), Task.WaitAll() or Task.WaitAny(). In this case, an instance of System.AggregateException is thrown that acts as a wrapper around one or more exceptions that have occurred. This is important for methods that coordinate multiple tasks like Task.WaitAll() and Task.WaitAny(), so the AggregateException is able to wrap all the exceptions within the running tasks that have occurred.
In the following example, we are creating and starting three tasks, two of which throw different exceptions. After starting these tasks, the main calling thread calls the WaitAll() method and catches the AggregateException. Finally, it iterates through the InnerExceptions property and prints out the details regarding the thrown exceptions. This property is the wrapper holding all the information about the aggregated exceptions.
- static void Main(string[] args)
- {
-
- Task task1 = new Task(() =>
- {
- NullReferenceException exception = new NullReferenceException();
- exception.Source = "task1";
- throw exception;
- });
-
- Task task2 = new Task(() =>
- {
- throw new IndexOutOfRangeException();
- });
-
- Task task3 = new Task(() =>
- {
- Console.WriteLine("Task 3");
- });
-
-
- task1.Start();
- task2.Start();
- task3.Start();
-
-
- try
- {
- Task.WaitAll(task1, task2, task3);
- }
- catch (AggregateException ex)
- {
-
- foreach (Exception inner in ex.InnerExceptions)
- {
- Console.WriteLine("Exception type {0} from {1}", inner.GetType(), inner.Source);
- }
- }
-
- Console.WriteLine("Main method complete. Press any key to finish.");
- Console.ReadKey();
- }
Image 4: Exceptions handling in tasks
Summary
In order to coordinate tasks, we use the instance method Wait() or one from the two static methods WaitAll()/WaitAny(). These methods allow the application either to wait until all the given tasks have completed or wait until any of the tasks completes first.
An exception that occurs during a task execution is not thrown until some trigger member is called. We use and handle the AggregateException that aggregates all the tasks exceptions that have been thrown.
In the next part of the series, we will take a closer look at tasks synchronization, discover synchronization primitives and learn how to share data among several running tasks properly to avoid deadlocks and data races.