Parallel
computing in the .NET Framework development relies on the concept of
tasks. This section is therefore about the core of the parallel
computing, and you learn to use tasks for scaling unit of works across
multiple threads and processors.
What Is a Task?
Different from the pure
threading world, in parallel computing the most important concept is
the task,
which is simply the basic unit of work, which can be scaled across all
available processors. A task is not a thread; a thread can run multiple
tasks, but each task can be also scaled across more than one thread,
depending on available resources. The task is therefore the most basic
unit of work for operations executed in parallel. In terms of code, a
task is nothing but an instance of the System.Threading.Tasks.Task class that holds a reference to a delegate, pointing to a method that does some work. The implementation is similar to what you do with Thread
objects but with the differences previously discussed. Basically you
have two alternatives for executing operations with tasks: The first
one is calling the Parallel.Invoke method; the second one is manually creating and managing instances of the Task class. The following subsections cover both scenarios.
Running Tasks with Parallel.Invoke
The first way for running tasks in parallel is calling the Parallel.Invoke shared method. This method can receive an array of System.Action objects as parameter, so each Action
is translated by the runtime into a task. If possible, tasks are
executed in parallel. The following example demonstrates how to perform
three calculations concurrently:
'Requires an Imports System.Threading.Tasks directive
Dim angle As Double = 150
Dim sineResult As Double
Dim cosineResult As Double
Dim tangentResult As Double
Parallel.Invoke(Sub()
Console.WriteLine(Thread.CurrentThread.
ManagedThreadId)
Dim radians As Double = angle * Math.PI / 180
sineResult = Math.Sin(radians)
End Sub,
Sub()
Console.WriteLine(Thread.CurrentThread.
ManagedThreadId)
Dim radians As Double = angle * Math.PI / 180
cosineResult = Math.Cos(radians)
End Sub,
Sub()
Console.WriteLine(Thread.CurrentThread.
ManagedThreadId)
Dim radians As Double = angle * Math.PI / 180
tangentResult = Math.Tan(radians)
End Sub)
In the example the code
takes advantage of statement lambdas; each of them is translated into a
task by the runtime that is also responsible for creating and
scheduling threads and for scaling tasks across all available
processors. If you run the code you can see how the tasks run within
separate threads, automatically created for you by the TPL. As an
alternative you can supply AddressOf
clauses pointing to methods performing the required operations, instead
of using statement lambdas. Although this approach is useful when you
need to run tasks in parallel the fastest way, it does not enable you
to take control over tasks themselves. This is instead something that requires explicit instances of the Task class, as explained in the next section.
Creating, Running, and Managing Tasks: The Task Class
The System.Threading.Tasks.Task class represents the unit of work in the parallel computing based on the .NET Framework. Differently from calling Parallel.Invoke, when you create an instance of the Task
class, you get deep control over the task itself, such as starting,
stopping, waiting for completion, and cancelation. The constructor of
the class requires you to supply a delegate or a lambda expression to
provide a method containing the code to be executed within the task.
The following code demonstrates how you create a new task and then
start it:
Dim simpleTask As New Task(Sub()
'Do your work here...
End Sub)
simpleTask.Start()
You supply the constructor with a lambda expression or with a delegate and then invoke the Start instance method. The Task class also exposes a Factory property of type TaskFactory
that offers members for interacting with tasks. For example, you can
use this property for creating and starting a new task all in one as
follows:
Dim factoryTask = Task.Factory.StartNew(Sub()
'Do your work here
End Sub)
This has the same result as the first code snippet. The logic is that you can create instances of the Task class, each with some code that will be executed in parallel.
When you launch a new task, the
task is executed within a managed thread. If you want to get
information on the thread, in the code for the task you can access it
via the System.Threading.Thread.CurrentThread shared property. For example, the CurrentThread.ManagedThreadId property will return the thread id that is hosting the task.
|
Creating Tasks That Return Values
The Task class also has a
generic counterpart that you can use for creating tasks that return a
value. For example consider the following code snippet that creates a
task returning a value of type Double, which is the result of
calculating the tangent of an angle:
Dim taskWithResult = Task(Of Double).
Factory.StartNew(Function()
Dim radians As Double _
= 120 * Math.PI / 180
Dim tan As Double = _
Math.Tan(radians)
Return tan
End Function)
Console.WriteLine(taskWithResult.Result)
Basically you use a Function, which represents a System.Func(Of T) so that you can return a value from your operation. The result is accessed via the Task.ResultResult
property contains the result of the tangent calculation. The problem is
that the start value on which the calculation is performed is
hard-coded. If you want to pass a value as an argument, you need to
approach the problem differently. The following code demonstrates how
to implement a method that receives an argument that can be reached
from within the new task: property. In the preceding example, the
Private Function CalcTan(ByVal angle As Double) As Double
Dim t = Task(Of Double).Factory.
StartNew(Function()
Dim radians As Double = angle * Math.PI / 180
tangentResult = Math.Tan(radians)
Return tangentResult
End Function)
Return t.Result
End Function
The result of the calculation is returned from the task. This result is wrapped by the Task.Result instance property, which is then returned as the method result.
Waiting for Tasks to Complete
You can explicitly wait for a task to complete by invoking the Task.Wait method. The following code waits until the task completes:
Dim simpleTask = Task.Factory.StartNew(Sub()
'Do your work here
End Sub)
simpleTask.Wait()
You can alternatively pass a number of milliseconds to the Wait method so that you can also check for a timeout. The following code demonstrates this:
simpleTask.Wait(1000)
If simpleTask.IsCompleted Then
'completed
Else
'timeout
End If
Notice how the IsCompleted property enables checking if the task is marked as completed by the runtime. Generally Wait has to be enclosed inside a Try..Catch block because the method asks the runtime to complete a task that could raise any exceptions. This is an example:
Try
simpleTask.Wait(1000)
If simpleTask.IsCompleted Then
'completed
Else
'timeout
End If
'parallel exception
Catch ex As AggregateException
End Try
Exception Handling
Handling exceptions is a
crucial topic in parallel programming. The problem is that multiple
tasks that run concurrently could raise more than one exception
concurrently, and you need to understand what the actual problem is.
The .NET Framework 4.0 offers the System.AggregateException class that wraps all exceptions occurred concurrently into one instance. The class then exposes, over classic properties, an InnerExceptions collection that you can iterate for checking what exceptions occurred. The following code demonstrates how you catch an AggregateException and how you iterate the instance:
Dim aTask = Task.Factory.StartNew(Sub() Console.
WriteLine("A demo task"))
Try
aTask.Wait()
Catch ex As AggregateException
For Each fault In ex.InnerExceptions
If TypeOf (fault) Is InvalidOperationException Then
'Handle the exception here..
ElseIf TypeOf (fault) Is NullReferenceException Then
'Handle the exception here..
End If
Next
Catch ex As Exception
End Try
Each item in InnerExceptions is an exception that you can verify with TypeOf.
Another problem is when you have tasks that run nested tasks that throw
exceptions. In this case you can take advantage of the AggregateException.Flatten
method, which wraps exceptions thrown by nested tasks into the parent
instance. The following code demonstrates how to accomplish this:
Dim aTask = Task.Factory.StartNew(Sub() Console.
WriteLine("A demo task"))
Try
aTask.Wait()
Catch ex As AggregateException
For Each fault In ex.Flatten.InnerExceptions
If TypeOf (fault) Is InvalidOperationException Then
'Handle the exception here..
ElseIf TypeOf (fault) Is NullReferenceException Then
'Handle the exception here..
End If
Next
Catch ex As Exception
End Try
Basically Flatten returns an instance of the AggregateException storing inner exceptions that included errors coming from nested tasks.
Cancelling Tasks
There are situations in
which you want to cancel task execution. To programmatically cancel a
task, you need to enable tasks for cancellation, which requires some
lines of code. You need an instance of the System.Threading.CancellationTokenSource class; this instance tells to a System.Threading.CancellationToken that it should be canceled. The CancellationToken class provides notifications for cancellation. The following lines declare both objects:
Dim tokenSource As New CancellationTokenSource()
Dim token As CancellationToken = tokenSource.Token
Then you can start a new task using an overload of the TaskFactory.StartNew method that takes the cancellation token as an argument. The following line accomplishes this:
Dim aTask = Task.Factory.StartNew(Sub() DoSomething(token), token)
You still pass a delegate as an argument; in the preceding example the delegate takes an argument of type CancellationToken that is useful for checking the state of cancellation during the task execution. The following code snippet provides the implementation of the DoSomething method, in a demonstrative way:
Sub DoSomething(ByVal cancelToken As CancellationToken)
'Check if cancellation was requested before
'the task starts
If cancelToken.IsCancellationRequested = True Then
cancelToken.ThrowIfCancellationRequested()
End If
For i As Integer = 0 To 1000
'Simulates some work
Thread.SpinWait(10000)
If cancelToken.IsCancellationRequested Then
'Cancellation was requested
cancelToken.ThrowIfCancellationRequested()
End If
Next
End Sub
The IsCancellationRequested property returns True if cancellation over the current task was requested. The ThrowIfCancellationRequested method throws an OperationCanceledException to communicate to the caller that the task was canceled. In the preceding code snippet the Thread.SpinWait
method simulates some work inside a loop. Notice how checking for
cancellation is performed at each iteration so that an exception can be
thrown if the task is actually canceled. The next step is to request
cancellation in the main code. This is accomplished by invoking the CancellationTokenSource.Cancel method, as demonstrated in the following code:
tokenSource.Cancel()
Try
aTask.Wait()
Catch ex As AggregateException
'Handle concurrent exceptions here...
Catch ex As Exception
End Try
Note
The OperationCanceledException is correctly thrown if Just My Code is disabled . If it is enabled, the compiler sends a message saying that an OperationCanceledException was unhandled by user code. This is benign, so you can simply go on running your code by pressing F5 again.
The Barrier Class
The System.Threading namespace in .NET 4.0 introduces a new class named Barrier.
The goal of this class is bringing a number of tasks that work
concurrently to a common point before taking further steps. Tasks work
across multiple phases and they signal they arrived at the barrier,
waiting for all other tasks to arrive. The constructor of the class
offers several overloads but all have in common the number of tasks
participating in the concurrent work. You can also specify the action
to take once they arrive at the common point (that is, they reach the
barrier and complete the current phase). Notice that the same instance
of the Barrier class can be used
multiple times, for representing multiple phases. The following code
demonstrates how three tasks reach the barrier after their work,
signaling the work completion and waiting for other tasks to finish:
Sub BarrierDemo()
' Create a barrier with three participants
' The Sub lambda provides an action that will be taken
' at the end of the phase
Dim myBarrier As New Barrier(3,
Sub(b)
Console.
WriteLine("Barrier has been " & _
"reached (phase number: {0})",
b.CurrentPhaseNumber)
End Sub)
' This is the sample work made by all participant tasks
Dim myaction As Action =
Sub()
For i = 1 To 3
Dim threadId As Integer =
Thread.CurrentThread.ManagedThreadId
Console.WriteLine("Thread {0} before wait.", threadId)
'Waits for other tasks to arrive at this same point:
myBarrier.SignalAndWait()
Console.WriteLine("Thread {0} after wait.", threadId)
Next
End Sub
' Starts three tasks, representing the three participants
Parallel.Invoke(myAction, myAction, myAction)
' Once done, disposes the Barrier.
myBarrier.Dispose()
End Sub
Basically the code performs these steps:
1. | Creates an instance of the Barrier class, adding three participants and specifying the action to take when the barrier is reached.
|
2. | Declares a common job for the three tasks (the myAction
object) which simply performs an iteration against running threads
simulating some work. When each task completes the work, the Barrier.SignalAndWait method is invoked. This tells the runtime to wait for other tasks to complete their work before going to the next phase.
|
3. | Launches the three concurrent tasks and disposes the myBarrier object at the appropriate time.
|
The code also reuses the
same Barrier instance in order to work across multiple phases. The
class also exposes interesting members such as:
AddParticipant and AddParticipants methods which respectively allow adding one or the specified number of participant tasks to the barrier
RemoveParticipant and RemoveParticipants methods which respectively allow removing one or the specified number of participant tasks from the barrier
CurrentPhaseNumber property of type Long, which returns the current phase number
ParticipantCount property of type Integer, which returns the number of tasks involved in the operation
ParticipantsRemaining property of type Integer, which returns the number of tasks that have not invoked the SignalAndWait method yet
A Barrier represents a single
phase in the process while multiple instances of the same Barrier
class, like in the preceding code, represent multiple phases.