You can start multiple tasks with the Parallel.Invoke method, create tasks with TaskFactory.StartNew, or use the Task
constructor. Each approach varies slightly in functionality but
ultimately creates a task that is scheduled and eventually started. So
far, the example tasks have been independent, with no relationship to
another task. However, tasks can have relationships. You can create continuationsubtasks, and tasks that have a parent-child relationship. Task relationships help you create more sophisticated solutions. tasks,
A continuation task
automatically starts after another task completes. For example, the
first task might be responsible for calculating a result, and then the
second task might display the result. An error might occur if the result
is shown before the calculation is complete. For this reason, it is
important to order the execution of these two tasks.
1. Continuation Tasks
Ordering parallel tasks is
sometimes helpful. Naturally, executing tasks in parallel is preferable;
however, for correctness, ordering of tasks is sometimes required. The
next image depicts four tasks. Two of the tasks are compute bound and
return a result. The other two tasks are responsible for displaying the
results. As shown, the four tasks are running in parallel, which could
cause problems.
The tasks should be ordered so
that the display tasks start after their corresponding compute tasks.
In this scenario, the compute task is termed the antecedent, and the display task is called the successor.
An antecedent is the first task in an ordered sequence. The successor
task is the second and is a continuation of the antecedent task. As illustrated, TaskB should continue TaskA, and TaskD should continue TaskC.
The Task class has several methods that order tasks. These methods schedule one task to continue after another. The ContinueWith method, which is an instance method, is the simplest of them. Call Task.ContinueWith
on the antecedent task. As a parameter, pass in the successor method as
a delegate. The parameter is used to create the successor task that
will continue after the antecedent task. Inside the successor task, you
can reference the antecedent task, which is provided as a parameter.
Create an antecedent and successor task and then wait for both to complete
Create a console application. In the Main function, create a new task by using the Task constructor. Initialize the task with a lambda expression. In the lambda expression, display the name of the task.
var antecedent = new Task(() =>{
Console.WriteLine("antecedent.");
});
Use the Task.ContinueWith
method to create a continuation task. In the lambda expression for this
task, display the name of the task. This task automatically runs when
the antecedent task completes.
var successor=antecedent.ContinueWith((firstTask) =>
{ Console.WriteLine("successor."); });
You can now start the antecedent task. Afterward, wait for both the antecedent and successor tasks to complete.
class Program
{
static void Main(string[] args)
{
var antecedent = new Task(() =>
{
Console.WriteLine("antecedent.");
});
var successor=antecedent.ContinueWith((firstTask) =>
{ Console.WriteLine("successor."); });
antecedent.Start();
Task.WaitAll(antecedent, successor);
}
}
In the previous example
code, the antecedent task did not return a result. When the antecedent
returns a value, the successor task can find the results of the
antecedent task in the Task.Result property. Remember, the successor gets a reference to the antecedent as a parameter.
Create an antecedent task and a successor task in which the antecedent task result is checked later
Create a console application. In the Main function, define a new task by using the Task
constructor. The task should return an integer value. Initialize the
task with a lambda expression. In the lambda expression, display the
current task and return a value. This is the antecedent task.
Task calculate = new Task(() =>{
Console.WriteLine("Calculate result.");
return 42;});
With the Task.ContinueWith
method, create a continuation task that displays the result of the
antecedent task. Pass a reference to the antecedent as a parameter. You
can use the reference to access the result of the antecedent.
var answer=calculate.ContinueWith((antecedent) =>{
Console.WriteLine("The answer is {0}.", antecedent.Result); });
You can now start the antecedent task. Then wait for both the antecedent and successor methods to complete.
class Program
{
static void Main(string[] args)
{
Task calculate = new Task(() =>
{
Console.WriteLine("Calculate result."); return 42;
});
Task answer=calculate.ContinueWith((antecedent) =>{
Console.WriteLine("The answer is {0}.", antecedent.Result); });
calculate.Start();
Task.WaitAll(calculate, answer);
}
}
In
the preceding example, the successor task started when the current task
completed. What if you want to continue only after several tasks
finish? That is possible with the static TaskFactory.ContinueWhenAll
method. This method accepts an array of tasks as a parameter. The
continuation task will begin after the last of these tasks has
completed. In this case, the successor task receives an array of
antecedent tasks as a parameter. You can use this array in the successor
to access state information from each antecedent.
Create two antecedent tasks and check the result of both in the successor task
Before the Main function, add a PerformCalculation method that returns an integer value of 42.
static int PerformCalculation() { return 42; }
Next,
create a new task that returns an integer value. Initialize the task
with a lambda expression. In the lambda expression, display the current
task and return the result of the PerformCalculation method.
Task TaskA = new Task(() =>{
Console.WriteLine("TaskA started.");
return PerformCalculation(); });
Now create a TaskB, similar to TaskA. TaskA and TaskB are the antecedent methods.
Task TaskB = new Task(() => {
Console.WriteLine("TaskB started.");
return PerformCalculation(); });
Create a continuation task to run after TaskA and TaskB have completed. You can accomplish this with the TaskFactory.ContinueWhenAll method. The first parameter is an array of tasks.
Task total=Task.Factory.ContinueWhenAll(new Task[] { TaskA, TaskB },
As part of TaskFactory.ContinueWhenAll,
you next define the continuation task as a lambda expression. Pass a
reference to the antecedent tasks as the parameter. In the successor
task, add and display the results of the antecedent tasks.
(tasks)=>Console.WriteLine("Total = {0}", tasks[0].Result+tasks[1].Result));
Start both antecedent tasks and then wait for both to complete. Afterward, wait for the continuation task to complete.
class Program
{
static int PerformCalculation() { return 42; }
static void Main(string[] args)
{
Task TaskA = new Task(() =>
{
Console.WriteLine("TaskA started.");
return PerformCalculation();
});
Task TaskB = new Task(() =>
{
Console.WriteLine("TaskB started.");
return PerformCalculation();
});
Task total=Task.Factory.ContinueWhenAll(new Task[] { TaskA, TaskB },
(tasks) => Console.WriteLine(
"Total = {0}", tasks[0].Result + tasks[1].Result));
TaskA.Start();
TaskB.Start();
Task.WaitAll(TaskA, TaskB);
total.Wait();
}
}
As shown, TaskFactory.ContinueWhenAll starts the continuation task (successor) after all the antecedent tasks have completed. Alternatively, there’s a TaskFactory.ContinueWhenAny, which is an instance method that starts the continuation task after any listed antecedent task completes.
An interesting option when continuing a task is the TaskContinuationOptions
parameter. With this option, you can set an event as an additional
criterion for starting the continuation task. For example, suppose you
want to continue a task only when the antecedent raises an exception. As
another example, you might want to continue a task only when the
antecedent was not canceled. TaskContinuationOptions is an enumeration that covers both of these scenarios and more. Here are the possible values.
None
AttachedToParent
ExecuteSynchronously
LongRunning
NotOnCanceled
NotOnFaulted
NotOnRanToCompletion
OnlyOnCanceled
OnlyOnFaulted
OnlyOnRanToCompletion
PreferFairness
Some of the values, such as OnlyOnFaulted, are not available for continuation or successor tasks with multiple antecedents.
Here
is a scenario. Assume that you want to perform a rollback if a task
throws an unhandled exception. Exceptions can sometimes leave objects in
an unknown state, so you might want to return objects to a known state;
being in an unknown state is rarely good for a program. If an unhandled
exception occurs in a task, you can perform the rollback in a
continuation task, by using the TaskContinuationOptions.OnlyOnFaulted enumeration value.
Implement the preceding scenario in an application
Create
a console application. You need to implement a custom task that can
perform a rollback of an operation. First, define a new CustomTask class. Inherit the class from the Task class.
class CustomTask : Task {
}
Implement a public one-argument constructor for the CustomTask class with an Action delegate as the parameter. Pass the Action delegate to the base class (Task) constructor.
public CustomTask(Action action)
: base(action)
{ }
Add a PerformRollback method to the CustomTask
class as a member method. In the real world, this method would perform a
rollback. In our example, it simply displays a message.
public void PerformRollback() { Console.WriteLine("Rollback..."); }
In the Main function, create a new CustomTask. This is the antecedent task. In the lambda expression for the task, throw an unhandled exception.
CustomTask antecedent = new CustomTask(() => {
throw new Exception("Unhandled"); });
Next, create a continuation task for the antecedent task by using the Task.ContinueWith
method. Implement the continuation task as a lambda expression. Pass a
reference of the antecedent task as a parameter of the lambda
expression. In our example, you want to perform a rollback. Finally, you
want to execute the continuation task only when the antecedent task has
a fault. For that reason, add the TaskContinuationOptions.OnlyOnFaulted as the final parameter.
antecedent.ContinueWith((predTask) =>
{
((CustomTask)predTask).PerformRollback();
}, TaskContinuationOptions.OnlyOnFaulted);
Now you can start the antecedent task. Wait for the task in a try/catch block.
class CustomTask : Task {
public CustomTask(Action action)
: base(action)
{ }
public void PerformRollback() { Console.WriteLine("Rollback..."); }
}
class Program
{
static void Main(string[] args)
{
CustomTask antecedent = new CustomTask(() =>
{
throw new Exception("Unhandled");
});
antecedent.ContinueWith((predTask) =>
{
((CustomTask)predTask).PerformRollback();
},
TaskContinuationOptions.OnlyOnFaulted);
antecedent.Start();
try
{
antecedent.Wait();
}
catch (AggregateException ex)
{
}
}
}