【问题标题】:Elegantly handle task cancellation优雅处理任务取消
【发布时间】:2012-09-19 23:28:33
【问题描述】:

将任务用于需要取消的大型/长时间运行的工作负载时,我经常使用与此类似的模板来执行任务:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException)
    {
        throw;
    }
    catch (Exception ex)
    {
        Log.Exception(ex);
        throw;
    }
}

OperationCanceledException 不应记录为错误,但如果任务要转换为已取消状态,则不得吞下。其他任何异常都不需要处理,超出本方法的范围。

这总是感觉有点笨拙,默认情况下,Visual Studio 会在 OperationCanceledException 的投掷时中断(尽管由于我使用了这种模式,我现在为 OperationCanceledException 关闭了“用户未处理中断”) .

更新:现在是 2021 年,C#9 提供了我一直想要的语法:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (Exception ex) when (ex is not OperationCanceledException)
    {
        Log.Exception(ex);
        throw;
    }
}
理想情况下,我想我希望能够做这样的事情:
public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (Exception ex) exclude (OperationCanceledException)
    {
        Log.Exception(ex);
        throw;
    }
}
罢工> 即,将某种排除列表应用于捕获,但没有当前不可能的语言支持(@eric-lippert:c# vNext 功能:))。

另一种方法是通过延续:

public void StartWork()
{
    Task.Factory.StartNew(() => DoWork(cancellationSource.Token), cancellationSource.Token)
        .ContinueWith(t => Log.Exception(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
}

public void DoWork(CancellationToken cancelToken)
{
    //do work
    cancelToken.ThrowIfCancellationRequested();
    //more work
}

但我不太喜欢这样,因为从技术上讲,异常可能有多个内部异常,并且在记录异常时没有像第一个示例中那样多的上下文(如果我做的不止只是记录它)。

我知道这是一个风格问题,但想知道是否有人有更好的建议?

我只需要坚持示例 1 吗?

【问题讨论】:

  • 我使用了一个解决方案,其中日志框架对某些异常执行不同的操作。即忽略 OperationCancelledException,展平 AggregateException,仅记录 InvalidOperationException 的 innerException 等

标签: c# exception task-parallel-library


【解决方案1】:

那么,有什么问题吗?扔掉catch (OperationCanceledException) 块,并设置适当的延续:

var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
    {
        var i = 0;
        try
        {
            while (true)
            {
                Thread.Sleep(1000);

                cts.Token.ThrowIfCancellationRequested();

                i++;

                if (i > 5)
                    throw new InvalidOperationException();
            }
        }
        catch
        {
            Console.WriteLine("i = {0}", i);
            throw;
        }
    }, cts.Token);

task.ContinueWith(t => 
        Console.WriteLine("{0} with {1}: {2}", 
            t.Status, 
            t.Exception.InnerExceptions[0].GetType(), 
            t.Exception.InnerExceptions[0].Message
        ), 
        TaskContinuationOptions.OnlyOnFaulted);

task.ContinueWith(t => 
        Console.WriteLine(t.Status), 
        TaskContinuationOptions.OnlyOnCanceled);

Console.ReadLine();

cts.Cancel();

Console.ReadLine();

TPL 区分取消和故障。因此,取消(即在任务主体中抛出 OperationCancelledException不是错误

要点:不要处理任务正文中的异常而不重新抛出它们。

【讨论】:

  • 我对延续方法的唯一问题是我失去了上下文。例如如果我正在处理一个项目列表,并且我需要记录在引发异常时我在集合中的距离。我确实错过了我现在提出的原始问题中的一些内容,那就是重新抛出异常 ex 以允许任务转换到故障状态。
  • 下面有一些更简单的答案。
【解决方案2】:

这是您优雅地处理任务取消的方式:

处理“即发即弃”任务

var cts = new CancellationTokenSource( 5000 );  // auto-cancel in 5 sec.
Task.Run( () => {
    cts.Token.ThrowIfCancellationRequested();

    // do background work

    cts.Token.ThrowIfCancellationRequested();

    // more work

}, cts.Token ).ContinueWith( task => {
    if ( !task.IsCanceled && task.IsFaulted )   // suppress cancel exception
        Logger.Log( task.Exception );           // log others
} );

处理等待任务完成/取消

var cts = new CancellationTokenSource( 5000 ); // auto-cancel in 5 sec.
var taskToCancel = Task.Delay( 10000, cts.Token );  

// do work

try { await taskToCancel; }           // await cancellation
catch ( OperationCanceledException ) {}    // suppress cancel exception, re-throw others

【讨论】:

    【解决方案3】:

    C# 6.0 对此有一个解决方案..Filtering exception

    int denom;
    
    try
    {
         denom = 0;
        int x = 5 / denom;
    }
    
    // Catch /0 on all days but Saturday
    
    catch (DivideByZeroException xx) when (DateTime.Now.DayOfWeek != DayOfWeek.Saturday)
    {
         Console.WriteLine(xx);
    }
    

    【讨论】:

    • 关键字实际上是'when'而不是'if'。语法(对于 OP)将是:catch( Exception ex ) when (!(ex is OperationCanceledException))
    • 使用 C# 9.0 的语法更好:catch (Exception ex) when (ex is not OperationCanceledException)
    【解决方案4】:

    你可以这样做:

    public void DoWork(CancellationToken cancelToken)
    {
        try
        {
            //do work
            cancelToken.ThrowIfCancellationRequested();
            //more work
        }
        catch (OperationCanceledException) when (cancelToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception ex)
        {
            Log.Exception(ex);
            throw;
        }
    }
    

    【讨论】:

    • 我更喜欢更严格的检查:catch (OperationCanceledException e) when (e.CancellationToken == cancelToken)
    • @OhadSchneider 通常(不在具体的简单示例中)不能保证cancelToken 将通过OperationCanceledException.CancellationToken 传播。链接令牌通常由异步方法在内部创建,隐藏原始CancellationToken 的身份。在大多数情况下,检查条件when (cancelToken.IsCancellationRequested) 是您能做的最好的事情。
    【解决方案5】:

    根据this MSDN blog post,你应该抓住OperationCanceledException,例如

    async Task UserSubmitClickAsync(CancellationToken cancellationToken)
    {
       try
       {
          await SendResultAsync(cancellationToken);
       }
       catch (OperationCanceledException) // includes TaskCanceledException
       {
          MessageBox.Show(“Your submission was canceled.”);
       }
    }
    

    如果您的可取消方法在其他可取消操作之间,您可能需要在取消时执行清理。这样做的时候可以使用上面的catch块,但是一定要正确地重新抛出:

    async Task SendResultAsync(CancellationToken cancellationToken)
    {
       try
       {
          await httpClient.SendAsync(form, cancellationToken);
       }
       catch (OperationCanceledException)
       {
          // perform your cleanup
          form.Dispose();
    
          // rethrow exception so caller knows you’ve canceled.
          // DON’T “throw ex;” because that stomps on 
          // the Exception.StackTrace property.
          throw; 
       }
    }
    

    【讨论】:

      【解决方案6】:

      我不完全确定您要在这里实现什么,但我认为以下模式可能会有所帮助

      public void DoWork(CancellationToken cancelToken)
      {
          try
          {
              //do work
              cancelToken.ThrowIfCancellationRequested();
              //more work
          }
          catch (OperationCanceledException) {}
          catch (Exception ex)
          {
              Log.Exception(ex);
          }
      }
      

      你可能已经注意到我已经从这里删除了 throw 语句。这不会抛出异常,只会忽略它。

      如果您打算做其他事情,请告诉我。

      还有另一种方式与您在代码中展示的方式非常接近

          catch (Exception ex)
          {
              if (!ex.GetType().Equals(<Type of Exception you don't want to raise>)
              {
                  Log.Exception(ex);
      
              }
          }
      

      【讨论】:

      • This:catch (OperationCanceledException) {} 会将任务的状态设置为RanToCompletion,而不是Canceled。有一些用例,当这是一个显着的差异。
      猜你喜欢
      • 2010-09-15
      • 2021-09-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多