Login | Register   
RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Getting Started with the .NET Task Parallel Library : Page 4

If you have a multi-core computer, chances are your first CPU does all the work while the others spend most of their time twiddling their electronic thumbs. Learn to unlock the idle power of your underused CPUs to greatly improve the performance of your applications.


Controlling Tasks
The Parallel.Invoke method lets you call a series of possibly different subroutines in parallel. The Parallel.For and Parallel.ForEach methods let you call the same subroutine many times, passing it either a sequence of numbers or a series of values in an IEnumerable. After the calls begin, all these methods—Parallel.Invoke, Parallel.For, and Parallel.ForEach—block your program until all the calls complete; then they return control to your program.

Occasionally you may want greater control over how the tasks execute. In particular, you may want to be able to cancel tasks. To give you that control, TPL provides the System.Threading.Tasks.Task class.

A Task object represents something that you want to do. You create a Task by calling the Task class's static Create method, passing it the address of the routine that you want it to execute.

After you create a Task, the program is free to execute the Task at any time. If there's a spare CPU sitting around with nothing to do, the Task will start executing right away.

The following code shows how example program TimedTasks prepares three tasks for execution. It makes an array of three Task objects that represent calls to the Task0, Task1, and Task2 subroutines.

   Private m_Tasks(2) As Tasks.Task
   ' Prepare the tasks.
   m_Tasks(0) = Task.Create(AddressOf Task0)
   m_Tasks(1) = Task.Create(AddressOf Task1)
   m_Tasks(2) = Task.Create(AddressOf Task2)
Author's Note: Technically, the subroutines Task0, Task1, and Task2 should take a single object as a parameter. While VB doesn't mind if the routines take no parameters, C# insists that they have the proper signature.

If the routines you're calling require a parameter, you can pass it as a second argument to Task.Create.

The following code creates an array of TaskData objects and then makes three tasks, passing each a different entry from the array.

   ' Prepare the task data.
   Private m_ElapsedTime(2) As TimeSpan
   Dim task_data() As TaskData = { _
       New TaskData() With {.TaskNum = 0, .NumSeconds = 2}, _
       New TaskData() With {.TaskNum = 1, .NumSeconds = 1}, _
       New TaskData() With {.TaskNum = 2, .NumSeconds = 3} _
   ' Prepare the tasks.
   m_Tasks(0) = Task.Create(AddressOf ParamTask, task_data(0))
   m_Tasks(1) = Task.Create(AddressOf ParamTask, task_data(1))
   m_Tasks(2) = Task.Create(AddressOf ParamTask, task_data(2))

The Task class provides several other methods to manage tasks in addition to Create:

  • The WaitAny method makes the program wait until any one of a set of tasks has finished.
  • The WaitAll method makes the program wait until all the tasks in a set are finished. WaitAll throws an error if any of the tasks is canceled.
  • The Task class's Cancel method cancels the task. This method doesn't actually stop the task; it simply sets its IsCanceled property to True. It is up to your task to check this value periodically to see if it should stop working.

Of course that begs the question, "How does the task know which Task is running it?" Example program TimedTasks solves this problem by making an array of Task objects and passing each task its index in the array.

Here's a more complete picture of how TimedTasks works. The following code allocates the Task array. It also creates an array of TimeSpan objects where the tasks can record their elapsed times when they finish. Giving each task a separate entry in the array lets them work independently, so they don't need to worry about interfering with each other.

   ' Run separate tasks.
   Private m_Tasks(2) As Task
   Private m_ElapsedTime(2) As TimeSpan

Next, the main program starts the tasks:

   ' Prepare the task data.
   Dim task_data() As TaskData = { _
   ' Prepare the tasks.
   ' Wait for any single task to finish (up to 10 seconds).
   Task.WaitAny(m_Tasks, 10000)
   ' Cancel all the remaining tasks.
   For i As Integer = 0 To m_Tasks.Length - 1
       If (Not m_Tasks(i).IsCanceled) AndAlso _
          (Not m_Tasks(i).IsCompleted) _
       End If
   Next i
   ' Wait for all tasks to finish.
   Catch ex As Exception
       lstResults.Items.Add("Exception: " & ex.Message)
       lstResults.Items.Add("    " & ex.InnerException.Message)
   End Try
   ' Display each tasks's results.
   For i As Integer = 0 To m_Tasks.Length - 1
       Dim txt As String = "Task " & i.ToString()
       If m_Tasks(i).IsCanceled Then
           txt &= " was canceled, "
       ElseIf m_Tasks(i).IsCompleted Then
           txt &= " completed, "
           txt &= " is still running, "
       End If
       txt &= task_data(i).ElapsedTime.TotalSeconds.ToString("0.00")
   Next i

This code creates an array of TaskData objects. An object's TaskNum property indicates the task's index in the m_Tasks array and the NumSeconds property tells how long the task should sleep.

The program then creates the Task objects, passing them their corresponding TaskData objects as parameters.

Next the program uses WaitAny to wait until one of the tasks finishes. The second parameter (10000) specifies the maximum number of milliseconds the program should wait.

In this example, where the tasks wait for 2, 1, and 3 seconds respectively, the second task finishes first after about 1 second.

Author's Note: In general, you should not assume that tasks will run in any particular order. On a dual-core machine, the program could run the two- and three-second tasks first and run the one-second task only after one of the others finished. On a quad-core machine, all three tasks would run at the same time.

When the first task finishes, the program loops through the m_Tasks array and calls the Cancel method for any tasks that are still running.

The program then uses WaitAll to wait until all the tasks finish. It uses a Try-Catch block to protect itself against the exception that WaitAll throws because a task was canceled.

The code finishes by displaying each task's status (canceled or finished, there should be no tasks still running) and its elapsed time.

The following code shows how the tasks work:

Private Sub ParamTask(ByVal task_data As TaskData)
       Dim start_time As Date = Now
       For i As Integer = 1 To task_data.NumSeconds * 10
           ' Do something time consuming.
           ' See if we have been canceled.
           If m_Tasks(task_data.TaskNum).IsCanceled Then Exit For
       Next i
Figure 2. Terrific Tasks: The Task class lets you control, wait for, and cancel tasks.

       Dim stop_time As Date = Now
       task_data.ElapsedTime = stop_time - start_time
       Debug.WriteLine("Task " & 
          task_data.TaskNum & " finished")
   End Sub

The ParamTask method records its start time and then enters a loop. For each second that the TaskData parameter indicates it should wait, the task sleeps for one second. It sleeps in little 100-millisecond naps so it can respond promptly if it is canceled.

Within the loop, the task checks its m_Tasks entry to see if it has been canceled. It uses the index in the TaskData object's TaskNum property to determine which m_Tasks entry to use. If the Task object's IsCanceled property is True, the task breaks out of its For loop.

The task finishes by saving its elapsed time in its TaskData object.

Figure 2 shows the output of program TimedTasks. The output in the list box shows that Task 1 (the 1-second task) completed after 1 second. Tasks 0 and 2 (the 2- and 3-second tasks) were canceled after 1.1 seconds.

Author's Note: All the sample programs are available in both VB.NET and C# versions.

Comment and Contribute








