取消任务将引发异常

根据我对 Tasks 的了解,下面的代码应该在不引发异常的情况下取消当前正在执行的任务。我的印象是,任务取消的全部意义在于礼貌地“请求”任务停止,而不必中止线程。

下列方案的产出是:

抛弃异常

[ OperationCanceledException ]

取消并返回最后一个计算素数。

我试图避免任何异常时取消。我如何才能做到这一点?

void Main()
{
var cancellationToken = new CancellationTokenSource();


var task = new Task<int>(() => {
return CalculatePrime(cancellationToken.Token, 10000);
}, cancellationToken.Token);


try
{
task.Start();
Thread.Sleep(100);
cancellationToken.Cancel();
task.Wait(cancellationToken.Token);
}
catch (Exception e)
{
Console.WriteLine("Dumping exception");
e.Dump();
}
}


int CalculatePrime(CancellationToken cancelToken, object digits)
{
int factor;
int lastPrime = 0;


int c = (int)digits;


for (int num = 2; num < c; num++)
{
bool isprime = true;
factor = 0;


if (cancelToken.IsCancellationRequested)
{
Console.WriteLine ("Cancelling and returning last calculated prime.");
//cancelToken.ThrowIfCancellationRequested();
return lastPrime;
}


// see if num is evenly divisible
for (int i = 2; i <= num/2; i++)
{
if ((num % i) == 0)
{
// num is evenly divisible -- not prime
isprime = false;
factor = i;
}
}


if (isprime)
{
lastPrime = num;
}
}


return lastPrime;
}
92259 次浏览

这一行显式地引发了一个 Exception:

cancelToken.ThrowIfCancellationRequested();

如果您想优雅地退出任务,那么您只需删除该行即可。

通常,人们将其用作一种控制机制,以确保当前处理被中止,而无需运行任何额外的代码。此外,在调用 ThrowIfCancellationRequested()时不需要检查是否取消,因为它在功能上等同于:

if (token.IsCancellationRequested)
throw new OperationCanceledException(token);

当使用 ThrowIfCancellationRequested()时,你的任务可能看起来更像这样:

int CalculatePrime(CancellationToken cancelToken, object digits) {
try{
while(true){
cancelToken.ThrowIfCancellationRequested();


//Long operation here...
}
}
finally{
//Do some cleanup
}
}

此外,如果标记被取消,Task.Wait(CancellationToken)将引发异常。要使用此方法,您需要将您的等待调用封装在 Try...Catch块中。

MSDN: 如何取消任务

我试图避免任何例外时取消。

你不该这么做。

抛出 OperationCanceledException是用 TPL 表示“您调用的方法已被取消”的惯用方法。不要反抗,只要期待。

这是一个 很好的东西,因为它意味着当你有多个操作使用相同的取消令牌时,你不需要在每一个级别检查你的代码,看看你刚刚调用的方法是否正常完成或者是否因为取消而返回。可以在任何地方都使用 CancellationToken.IsCancellationRequested,但是从长远来看,它会使您的代码不那么优雅。

请注意,在您的示例中有一些 代码片段正在抛出一个异常-一个在任务本身内部:

cancelToken.ThrowIfCancellationRequested()

一种是等待任务完成:

task.Wait(cancellationToken.Token);

我不认为你真的想传递取消令牌到 task.Wait调用,老实说... 这允许 其他代码取消 你的等待。假设您知道您已经取消了令牌,那么这是没有意义的——抛出异常是 约束,无论任务是否已经注意到取消。选择:

  • 使用 与众不同取消令牌(以便其他代码可以独立取消等待)
  • 暂停一下
  • 等多久就等多久

另一个关于使用 ThrowIfCancellationRequested而不是 IsCancellationRequested的好处的注意事项: 我发现当需要使用 ContinueWithTaskContinuationOptions.OnlyOnCanceled的延续选项时,IsCancellationRequested不会导致条件 ContinueWith激活。但是,威尔设置任务的“取消”条件,导致 ContinueWith触发。

注意: 只有当任务已经在运行时才会出现这种情况,而在任务开始运行时则不会出现这种情况。这就是为什么我在开始和取消之间添加了 Thread.Sleep()

CancellationTokenSource cts = new CancellationTokenSource();


Task task1 = new Task(() => {
while(true){
if(cts.Token.IsCancellationRequested)
break;
}
}, cts.Token);
task1.ContinueWith((ant) => {
// Perform task1 post-cancellation logic.
// This will NOT fire when calling cst.Cancel().
}


Task task2 = new Task(() => {
while(true){
cts.Token.ThrowIfCancellationRequested();
}
}, cts.Token);
task2.ContinueWith((ant) => {
// Perform task2 post-cancellation logic.
// This will fire when calling cst.Cancel().
}


task1.Start();
task2.Start();
Thread.Sleep(3000);
cts.Cancel();

上面的一些答案读起来好像 ThrowIfCancellationRequested()是一个选项。在这种情况下是 没有,因为不会得到最后的素数。idiomatic way that "the method you called was cancelled"定义为取消意味着抛弃任何(中间)结果的情况。如果您对取消的定义是“停止计算并返回最后一个中间结果”,那么您已经使用了这种方式。

讨论这些好处,特别是在运行时间方面,也很容易产生误导: 实现的算法在运行时很糟糕,即使是高度优化的取消也不会有任何好处。

最简单的优化是展开这个循环并跳过一些不必要的循环:

for(i=2; i <= num/2; i++) {
if((num % i) == 0) {
// num is evenly divisible -- not prime
isprime = false;
factor = i;
}
}

你可以的

  • 对每个偶数保存(num/2)-1个循环,总体略低于50% (展开) ,
  • 为每个素数保存(num/2)-(num)循环的平方根(根据最小素数因子的数学选择界) ,
  • 至少为每个非素数节省这么多,期望更多的节省,例如 num = 999以1个周期结束,而不是499(如果找到答案,则中断)和
  • 节省另外50% 的周期,当然总共是25% (根据素数的数学选择步骤,展开处理特殊情况2)。

这意味着在内部循环中保证最少节省75% (粗略估计: 90%)的循环,只需将其替换为:

if ((num % 2) == 0) {
isprime = false;
factor = 2;
} else {
for(i=3; i <= (int)Math.sqrt(num); i+=2) {
if((num % i) == 0) {
// num is evenly divisible -- not prime
isprime = false;
factor = i;
break;
}
}
}

有更快的算法(我不会讨论这个问题,因为我离题太远了) ,但是这个优化非常简单,仍然证明了我的观点: 当算法 这个远非最优时,不要担心微优化运行时。

有两个东西在监听这个令牌,计算素数方法和任务实例。计算素数方法应该优雅地返回,但任务在运行时被取消,因此抛出。当您构造任务时,不必麻烦给它标记。