.NET包含一个取消架构,允许以标准方式取消长时间运行的任务。每个阻塞调用都应该支持这种机制。当然目前并不是所有阻塞调用都实现了这个新技术,但越来越多的阻塞调用都支持它。已经提供了这种机制的技术有任务、并发集合类、并行LINQ和几种同步机制。
取消架构基于协作行为,它不是强制的。长时间运行的任务会检查它是否被取消,并相应地返回控制权。
支持取消的方法接受一个CancellationToken参数。这个类定义了IsCancellationRequested属性,其中长时间运行的操作可以检查它是否应终止。长时间运行的操作检查取消的其他方式有:取消标记时,使用标记的WaitHandle属性,或者使用Register()方法。Register()方法接受Action和ICancelableOperation类型的参数。Action委托引用的方法在取消标记时调用。这类似于ICancelableOperation,其中实现这个接口的对象的Cancel()方法在执行取消操作时调用。
CancellationSamples的示例代码使用如下名称空间:
System
System.Threading
System.Threading.Tasks
1. Parallel.For()方法的取消
本节以一个使用Parallel.For()方法的简单例子开始。Parallel类提供了For()方法的重载版本,在重载版本中,可以传递ParallelOptions类型的参数。使用ParallelOptions类型,可以传递一个CancellationToken参数。CancellationToken参数通过创建CancellationTokenSource来生成。由于CancellationTokenSource实现了ICancelableOperation接口,因此可以用CancellationToken注册,并允许使用Cancel()方法取消操作。本例没有直接调用Cancel()方法,而是使用了方法CancelAfter(),在500毫秒后取消标记。
在For()循环的实现代码内部,Parallel类验证CancellationToken的结果,并取消操作。一旦取消操作,For()方法就抛出一个OperationCancelException类型的异常,这是本例捕获的异常。使用CancellationToken可以注册取消操作时的信息。为此,需要调用Register()方法,并传递一个在取消操作时调用的委托。
static void CancelParallelFor()
{
var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("*** token cancelled"));
//send a cancel after 500 ms
cts.CancelAfter(500);
try
{
ParallelLoopResult result = Parallel.For(0,100,new ParallelOptions
{
CancellationToken = cts.Token
},
x=>
{
Console.WriteLine($"loop {x} started");
int sum = 0;
for (int i = 0; i < 100; i++)
{
Task.Delay(2).Wait();
sum += i;
}
Console.WriteLine($"loop {x} finished");
});;
}
catch (OperationCanceledException ex)
{
Console.WriteLine(ex.Message);
}
}
运行应用程序,会得到类似如下的结果,第16、64、0、32、48、80和96次迭代都启动了。这在一个有6个内核CPU的系统上运行。通过取消操作,所有其他的迭代操作都在启动之前就取消了。启动的迭代操作允许完成,因为取消操作总是以协作方式运行,以避免在取消迭代操作的中间泄漏资源。
loop 16 started
loop 64 started
loop 0 started
loop 32 started
loop 48 started
loop 80 started
loop 96 started
*** token cancelled
loop 48 finished
loop 64 finished
loop 96 finished
loop 32 finished
loop 16 finished
loop 0 finished
loop 80 finished
The operation was canceled.
2. 任务的取消
同样的取消模式也可用于任务。首先,新建一个CancellationTokenSource。如果仅需要一个取消标记,就可以通过访问Task.Factory.CancellationToken以使用默认的取消标记。接着,与前面的代码类似,在500毫秒后取消任务。在循环中执行主要工作的任务通过TaskFactory对象接受取消标记。在构造函数中,把取消标记赋予TaskFactory。这个取消标记由任务用于检查CancellationToken的IsCancellationRequested属性,以确定是否请求了取消。(该段文字描述与下面的示例代码不符)
static void CancelTask()
{
var cts = new CancellationTokenSource();
cts.Token.Register(() => Console.WriteLine("*** task cancelled"));
//send a cancel after 500 ms
cts.CancelAfter(500);
Task t1 = Task.Run(() =>
{
CancellationToken token = cts.Token;
Console.WriteLine("in task");
for (int i = 0; i < 20; i++)
{
Task.Delay(100).Wait();
if (token.IsCancellationRequested)
{
Console.WriteLine("cancelling was requested,cancelling from within the task");
token.ThrowIfCancellationRequested();
break;
}
Console.WriteLine("in loop");
}
Console.WriteLine("task finished without cancellation");
}, cts.Token);
try
{
t1.Wait();
}
catch (AggregateException ex)
{
Console.WriteLine($"exception: {ex.GetType().Name}, {ex.Message}");
foreach (var innerException in ex.InnerExceptions)
{
Console.WriteLine($"inner exception: {ex.InnerExceptions.GetType().Name}, {ex.InnerException.Message}");
}
}
}
运行应用程序,可以看到任务启动了,运行了几个循环,并获得了取消请求。之后取消任务,并抛出AggregateException异常,它是从方法调用ThrowCancellationRequester()中启动的。调用者等待任务时,会捕获AggregateException异常,它包含内部异常。例如,如果在一个也被取消的任务中运行Parallel.For()方法,这就可以用于取消的层次结构。任务的最终状态是Canceled。
运行结果:
in task
in loop
in loop
in loop
*** task cancelled
cancelling was requested,cancelling from within the task
exception: AggregateException, One or more errors occurred. (A task was canceled.)
inner exception: ReadOnlyCollection`1, A task was canceled.