29.6.3 后续任务选项
为了便于叙述,在进一步学习之前先来统一下名词。假设有两个任务,按照从上至下的顺序,上面的任务相对下面的任务叫做“后续任务”(又叫“延续任务”),而上面的任务则称作“前置任务”,如图29-7所示。
图 29-7 “前置任务”和“后续任务”
和后续任务有关的选项不止一个,它包括4大类:
❑任务创建选项。与前文已介绍过的PreferFairness、LongRunning、AttachedToParent基本一致,这里不再做详细介绍。
❑触发选项。用于控制后续任务在何时创建或者取消,主要基于前置任务的运行状态。
❑同步执行选项。控制后续任务是否使用内联方式运行。
❑默认选项。和前面几类任务相关选项的默认选项类似。
一个后续任务一般情况是没有创建选项、触发选项的(这意味着后续任务将不管前置任务是否完成或者取消都将无条件执行),也不使用内联方式运行。我们现在将学习如何使用这些选项来控制一个后续任务。
1.默认选项
基于默认选项,一个后续认为将具有如下特性:
❑无论前置任务的最终运行结果如何(顺利完成、被取消还是抛出异常终止),后续任务都将无条件开始运行;
❑基于异步模式运行;
❑不使用任何“任务创建选项”(PreferFairness、LongRunning、AttachedToParent)。
2.触发选项
一个任务的结束状态总是如下三种之一:
❑失败(Faulted)
❑被取消(Canceled)
❑顺利完成(RanToCompletion)
触发时机选项允许控制一个后续任务的启动条件。也就是说,当指定了触发时机选项选项,后续任务将仅仅在选项和前置任务的最终状态匹配时才触发,反之如果条件不匹配,那么后续任务将被取消执行。
需要注意的是,像TaskFactory.ContinueWhenAll和TaskFactory.ContinueWhenAny这类具有多个前置任务的方法,是不允许使用触发选项的,否则将触发一个ArgumentOutOfRange异常。目前,只接受一个前置任务的ContinueWith方法才能使用触发参数。未来的.NET版本将支持在ContinueWhenAll和ContinueWhenAny方法中使用触发选项。
接下来将介绍各个触发选项,这些参数是:
❑TaskContinuationOptions.NotOnRanToCompletion:前置任务完成则后续任务取消运行。
❑TaskContinuationOptions.NotOnFaulted:前置任务失败则后续任务取消运行。
❑TaskContinuationOptions.NotOnCanceled:前置任务取消则后续任务取消运行。
❑TaskContinuationOptions.OnlyOnRanToCompletion:前置任务完成则后续任务才运行。
❑TaskContinuationOptions.OnlyOnFaulted:前置任务错误则后续任务才运行。
❑TaskContinuationOptions.OnlyOnCanceled:前置任务取消则后续任务才运行。
(1)前置任务完成不运行
TaskContinuationOptions.NotOnRanToCompletion选项指示后续任务如果前置任务完成则不运行。如果前置任务运行完成,则后续任务就被取消,考虑代码清单29-20:
代码清单29-20 TaskContinuationOptions.NotOnRanToCompletion选项示例
using System;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
Task antecedent=Task.Factory.StartNew(()=>{});
bool result=false;
Task c1=antecedent.ContinueWith(_=>{result=true;},
TaskContinuationOptions.NotOnRanToCompletion);
Console.WriteLine("result={0}",result);
}
}
}
因为antecedent任务顺利完成,并且c1任务使用NotOnRanToCompletion触发选项,因此c1任务将被取消运行,result变量的最终结果将为false,如下为运行结果:
result=False
请按任意键继续……
该选项的一个应用场景是:如果前置任务出错或被取消,则后续任务可以清理资源或者进行错误报告,考虑下代码清单29-21:
代码清单29-21 TaskContinuationOptions.NotOnRanToCompletion选项的另一个示例
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
CancellationTokenSource tokenSource=new CancellationTokenSource();
CancellationToken token=tokenSource.Token;
Task antecedent=Task.Factory.StartNew(()=>
{
while(!token.IsCancellationRequested)
{
Console.WriteLine(“antecedent任务正在运行!”);
}
},token);
tokenSource.Cancel();
Task c1=antecedent.ContinueWith(_=>
{
Console.WriteLine(“后续任务运行,前置任务取消了!”);
},TaskContinuationOptions.NotOnRanToCompletion);
Thread.Sleep(1000);
}
}
}
其运行结果如下:
后续任务运行,前置任务取消了!
请按任意键继续……
(2)前置任务错误不运行
TaskContinuationOptions.NotOnFaulted选项指示如果前置任务失败(例如:异常)则后续任务取消。考虑代码清单29-22。
代码清单29-22 TaskContinuationOptions.NotOnFaulted选项示例代码
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
Task antecedent=Task.Factory.StartNew(()=>
{
Console.WriteLine(“后续任务不会运行,因为前置任务出现异常!”);
throw new Exception();
});
Task c1=antecedent.ContinueWith(_=>
{
Console.WriteLine(“后续任务运行,前置任务正常完成!”);
},TaskContinuationOptions.NotOnFaulted);
Thread.Sleep(1000);
}
}
}
因为antecedent任务中出现了异常,并且c1任务使用了NotOnFaulted选项,因此后续任务不会执行。运行结果如下:
后续任务不会运行,因为前置任务出现异常!
请按任意键继续……
(3)前置任务取消不运行
TaskContinuationOptions.NotOnCanceled选项指示如果前置任务被取消,则后续任务也将取消运行,考虑代码清单29-23。
代码清单29-23 TaskContinuationOptions.NotOnCanceled选项示例代码
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
CancellationTokenSource tokenSource=new CancellationTokenSource();
CancellationToken token=tokenSource.Token;
bool result=false;
Task antecedent=Task.Factory.StartNew(()=>
{
while(!token.IsCancellationRequested)
{
Console.WriteLine(“前置任务正在运行。”);
}
},token);
tokenSource.Cancel();
Task c1=antecedent.ContinueWith(_=>
{
result=true;
Console.WriteLine("result={0}",result);
},TaskContinuationOptions.NotOnCanceled);
Thread.Sleep(1000);
}
}
}
因为前置任务antecedent被取消,并且后续任务c1使用了NotOnCanceled选项,因此也将取消运行,因此为result变量赋值true将不被运行,result的值仍将为false。
(4)前置任务完成才运行
TaskContinuationOptions.OnlyOnRanToCompletion选项指示仅在前置任务完成后,后续任务才启动运行。考虑代码清单29-24。
代码清单29-24 TaskContinuationOptions.OnlyOnRanToCompletion选项示例代码
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
CancellationTokenSource tokenSource=new CancellationTokenSource();
CancellationToken token=tokenSource.Token;
bool result=false;
Task antecedent=Task.Factory.StartNew(()=>
{
while(!token.IsCancellationRequested)
{
Console.WriteLine(“前置任务正在运行。”);
}
},token);
tokenSource.Cancel();
Task c1=antecedent.ContinueWith(_=>
{
result=true;
Console.WriteLine("result={0}",result);
},TaskContinuationOptions.OnlyOnRanToCompletion);
Thread.Sleep(1000);
}
}
}
前置任务antecedent被取消,但c1任务使用了OnlyOnRanToCompletion选项,因此只有在前置任务完成时才执行,故c1任务将被取消,因此result变量的最终值仍将是false。
此选项在后续任务需要使用前置任务的运行结果时非常有用,考虑代码清单29-25。
代码清单29-25 后续任务使用前置任务的运行结果
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
CancellationTokenSource tokenSource=new CancellationTokenSource();
CancellationToken token=tokenSource.Token;
Task<string>antecedent=Task<string>.Factory.StartNew(()=>
{
return"success";
},token);
//tokenSource.Cancel();
Task c1=antecedent.ContinueWith(_=>
{
Console.WriteLine(antecedent.Result);
},TaskContinuationOptions.OnlyOnRanToCompletion);
Thread.Sleep(1000);
}
}
}
前置任务返回一个string类型的值,注意我们注释掉了取消前置任务运行的代码,也就意味着前置任务可以正常结束,而在后续任务c1中使用了前置任务的运行结果,这里只是简单地输出这一结果,运行结果如下所示:
success
请按任意键继续……
(5)前置任务错误才运行
TaskContinuationOptions.OnlyOnFaulted选项指示只有前置任务错误(例如:异常)时后续任务才运行,考虑代码清单29-26。
代码清单29-26 TaskContinuationOptions.OnlyOnFaulted选项示例代码
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
bool result=false;
Task antecedent=Task.Factory.StartNew(()=>
{
throw new Exception();
});
Task c1=antecedent.ContinueWith(_=>
{
result=true;
Console.WriteLine("result={0}",result);
},TaskContinuationOptions.OnlyOnFaulted);
Thread.Sleep(1000);
}
}
}
前置任务中出现了异常,而后续任务c1中使用了OnlyOnFaulted,因此后续任务将得以执行,运行结果如下:
result=True
请按任意键继续……
(6)前置任务取消才运行
TaskContinuationOptions.OnlyOnCanceled选项指示只有前置任务被取消后续任务才可以运行,考虑代码清单29-27。
代码清单29-27 TaskContinuationOptions.OnlyOnCanceled选项示例代码
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
CancellationTokenSource tokenSource=new CancellationTokenSource();
CancellationToken token=tokenSource.Token;
bool result=false;
Task antecedent=Task.Factory.StartNew(()=>
{
while(!token.IsCancellationRequested)
{
Console.WriteLine(“前置任务正在运行。”);
}
},token);
tokenSource.Cancel();
Task c1=antecedent.ContinueWith(_=>
{
result=true;
Console.WriteLine("result={0}",result);
},TaskContinuationOptions.OnlyOnCanceled);
Thread.Sleep(1000);
}
}
}
前置任务被取消,而后续任务c1使用了OnlyOnCanceled选项,因此c1得以执行,运行结果如下:
result=True
请按任意键继续……
(7)一个综合示例
考虑代码清单29-28。
代码清单29-28 使用各种触发选项的综合示例
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
int count=0;
Task A=Task.Factory.StartNew(()=>
{
count++;
});
Task B=A.ContinueWith(_=>
{
count++;
},TaskContinuationOptions.OnlyOnFaulted);
Task C=B.ContinueWith(_=>
{
count++;
},TaskContinuationOptions.NotOnCanceled);
Task D=C.ContinueWith(_=>
{
count++;
},TaskContinuationOptions.NotOnRanToCompletion);
Task E=D.ContinueWith(_=>
{
count++;
},TaskContinuationOptions.OnlyOnCanceled);
Task F=E.ContinueWith(_=>
{
count++;
Console.WriteLine(count);
},TaskContinuationOptions.None);
Thread.Sleep(1000);
}
}
}
先分析一下上述代码中count变量最后的值究竟为多少?分析过程如下:
❑A任务完成后count的值为1;
❑B任务被取消执行,因为它使用OnlyOnFaulted选项,只有前置任务A失败才会执行;
❑C任务被取消执行,因为NotOnCanceled要求前置任务B取消执行了;
❑D任务会执行,因为NotOnRanToCompletion表示前置任务C正常完成则不执行,现在C被取消了,因此D得以执行,此时count的值为2;
❑E任务被取消执行,因为OnlyOnCanceled指示只有D任务被取消,E任务才会执行;
❑F任务会执行,因为它使用了默认设置,那么不管前面的任务最终结果如何,都将执行,此时count值为3。
3.同步执行
一般的,一个后续任务会使用异步模式运行。也就是说,前置任务运行一结束,后续任务就被提交到任务计划程序以待执行。然而,如果使用ExecuteSynchronously选项创建后续任务,表示应同步执行延续任务。可能有两种方式:
❑指定此选项后,延续任务将在导致前面的任务转换为其最终状态的相同线程上运行。
❑如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。
在内联方式下运行的任务,性能要好于一般的同步模式,因为它避免了提交后续任务到计划程序入队和离队的开销。需要注意的是,只应同步执行运行时间非常短的延续任务。
考虑下代码清单29-29。
代码清单29-29 ExecuteSynchronously选项示例程序
u
using System.Threading;
using System.Threading.Tasks;
namespace ProgrammingCSharp4
{
class ParallelSample
{
public static void Main(string[]args)
{
Task<int>t1=Task<int>.Factory.StartNew(()=>
{
return 1;
});
Task<int>t2=Task<int>.Factory.StartNew(()=>
{
return 2;
});
Task<int>t3=Task<int>.Factory.StartNew(()=>
{
return 3;
});
Task<int>addTask=Task.Factory.ContinueWhenAll(
new Task<int>[]{t1,t2,t3},preTasks=>
{
int sum=0;
for(int i=0;i<preTasks.Length;i++)
s
i
n
g
S
y
s
t
e
m
;
{
sum+=preTasks[i].Result;
}
return sum;
},TaskContinuationOptions.ExecuteSynchronously);
Console.WriteLine("sum={0}",addTask.Result);
}
}
}
在上述代码中,任务addTask用于将t1、t2和t3任务的结果值相加,因为任务比较简单,因此很适合同步运行。记得不要使用此选项创建比较耗时的任务。