取消任务正在抛出异常

Vin*_*cio 55 c# multithreading c#-4.0

从我读到的有关任务的内容来看,以下代码应该取消当前正在执行的任务而不会抛出异常.我的印象是,任务取消的重点是礼貌地"要求"任务停止而不中止线程.

以下程序的输出是:

倾倒异常

[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;
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 89

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

你不应该这样做.

投掷OperationCanceledException是在TPL中表达"你所取消的方法被取消"的惯用方式.不要反对 - 只是期待它.

这是一个很好的事情,因为这意味着当你有使用相同的取消标记多个操作,你不需要辣椒代码在与检查每一个层面看你刚刚调用的方法是否具有实际正常完成或是否因取消而退回.你可以CancellationToken.IsCancellationRequested随处使用,但从长远来看,它会让你的代码变得不那么优雅.

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

cancelToken.ThrowIfCancellationRequested()
Run Code Online (Sandbox Code Playgroud)

还有一个等待任务完成的地方:

task.Wait(cancellationToken.Token);
Run Code Online (Sandbox Code Playgroud)

我不认为你真的想要将取消令牌传递给task.Wait电话,说实话......这允许其他代码取消你的等待.鉴于你知道你刚刚取消了这个令牌,这是毫无意义的 - 它必然会抛出异常,无论该任务是否实际上已经注意到取消.选项:

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

  • @Bobb:注意,属性(`IsCancellationRequested`)和方法(`ThrowIfCancellationRequested`)*都是在TPL中实现的.因此,在*非常极端的情况下*你真的不想要一个例外的成本,你可以避免它 - 但是如果没有*证据*它实际上是系统中的瓶颈,我就不会这样做. (10认同)
  • @TimBarrass:我觉得有点不对劲,但我可以忍受它.如果您认为异常不是*错误*,而是作为一种说法"我没有完成您要求我做的操作 - 这就是为什么",那么它可能更有意义.不使用流量控制异常的咒语可能有点过分 - 它们总是*在某种程度上用于流量控制.更重要的是不在正常的成功路径中使用它们. (8认同)
  • 我目前遇到这个问题,主要是因为取消似乎是一个正常的操作,并且使用异常进行正常的流量控制*stll*感觉不对.我错过了什么? (5认同)
  • @AbhijeetPatel:这是非常依赖于上下文的,但通常你会尝试对其进行编码,以便在任务取消时无关紧要. (3认同)
  • @JonSkeet No answer 对这里的另一个回复有很好的评论——注意取消是一种罕见/开关事件,因此基于性能的批评(例如)可能基本上无关紧要。但与您的回复更密切相关——我突然想到,在某种意义上,取消 ** 是 ** _exceptional_ ...... / 微微眯眼 (2认同)

Jos*_*osh 69

您在此行明确抛出异常:

cancelToken.ThrowIfCancellationRequested();
Run Code Online (Sandbox Code Playgroud)

如果你想优雅地退出任务,那么你只需要摆脱那条线.

通常人们使用它作为控制机制来确保当前处理被中止而不会运行任何额外的代码.此外,在呼叫时无需检查取消,ThrowIfCancellationRequested()因为它在功能上等同于:

if (token.IsCancellationRequested) 
    throw new OperationCanceledException(token);
Run Code Online (Sandbox Code Playgroud)

使用ThrowIfCancellationRequested()您的任务可能看起来更像这样:

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

            //Long operation here...
        }
    }
    finally{
        //Do some cleanup
    }
}
Run Code Online (Sandbox Code Playgroud)

此外,Task.Wait(CancellationToken)如果令牌被取消,将抛出异常.要使用此方法,您需要将Wait调用包装在一个Try...Catch块中.

MSDN:如何取消任务

  • 作为一个警告:正如这个答案所说,用'IsCancellationRequested`的一堆检查替换`ThrowIfCancellationRequested`正好退出.但这不仅仅是一个实施细节; 这会影响可观察的行为:任务将不再以取消状态结束,而是在"RanToCompletion"中结束.***不仅可以影响显式状态检查,而且可以更简单地影响任务链接,例如`ContinueWith`,具体取决于所使用的`TaskContinuationOptions`.我会说避免"ThrowIfCancellationRequested"是危险的建议. (36认同)
  • 谢谢乔希.在循环的每次迭代中调用ThrowIfCancellationRequested()而不是加倍并检查取消标志是有意义的. (2认同)

No *_*wer 9

上面的一些答案看起来好像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; 
  }
} 
Run Code Online (Sandbox Code Playgroud)

您可以

  • 每个偶数保存(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;
    }
  }
} 
Run Code Online (Sandbox Code Playgroud)

有更快的算法(我将不讨论,因为我是远远不够题外话),但这优化是很容易的,还是证明我的观点:不要担心微观优化的运行时,当你的算法是这样远最佳.

  • 对不起,我和我最初不想要的话题一样偏离了主题.然而,我对"280Z28 1月15日1:37"的回答中遗漏的一点是,保证75%的内循环可以使任何"**取消**运行时讨论"过时.考虑到每次调用取消每个定义只执行一次,因此不应该有很多应用程序需要担心其运行时. (3认同)

小智 8

关于使用ThrowIfCancellationRequested而不是的好处的另一个注意事项IsCancellationRequested:我发现当需要使用ContinueWith延续选项时TaskContinuationOptions.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();
Run Code Online (Sandbox Code Playgroud)

  • 这是因为只有当任务以给定的异常类型退出时,任务状态才会设置为“已取消”,否则,如果您只是“返回”它,则其状态会设置为 RanToCompletion。因此,不会调用 OnlyOnCanceled 代码段。 (2认同)