取消任务将抛出异常

本文关键字:抛出异常 任务 取消 | 更新日期: 2023-09-27 18:08:10

从我对任务的了解来看,下面的代码应该取消当前正在执行的任务而不会抛出异常。在我的印象中,任务取消的全部意义在于礼貌地"请求"任务停止,而不中止线程。

下面程序的输出是:

倾销异常[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;
}

取消任务将抛出异常

我正在努力避免取消时出现任何异常。

你不应该那样做。

抛出OperationCanceledException是TPL中"你调用的方法被取消"的惯用表达方式。不要抗拒,只要期待就好。

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

注意,在你的例子中有两个代码片段抛出了一个异常——一个在任务本身中:

cancelToken.ThrowIfCancellationRequested()

和等待任务完成的地方:

task.Wait(cancellationToken.Token);

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

  • 使用不同的取消令牌(以便其他代码可以独立取消您的等待)
  • 使用超时
  • 只要等待,只要需要

你在这一行显式抛出了一个异常:

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)将抛出异常。要使用此方法,您需要将Wait调用封装在Try...Catch块中。

MSDN:如何取消任务

上面的一些答案读起来好像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%的整体(展开),
  • save (num/2)-square_root_of(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;
    }
  }
} 

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

关于使用ThrowIfCancellationRequested而不是IsCancellationRequested的好处的另一个注意事项:我发现当需要使用ContinueWithTaskContinuationOptions.OnlyOnCanceled的延续选项时,IsCancellationRequested不会导致条件ContinueWith触发。然而,ThrowIfCancellationRequested 设置任务的取消条件,导致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();

有两个东西监听令牌,一个是calculate prime方法,另一个是名为Task的Task实例。calculate prime方法应该优雅地返回,但任务在运行时被取消,因此会抛出。当你构造task时,不要费心给它标记。