39

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

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吗?

4

6 回答 6

18

所以有什么问题?只需扔掉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在任务主体内抛出)不是故障

要点:不要在不重新抛出异常的情况下处理任务主体内的异常。

于 2012-09-28T05:39:19.627 回答
13

以下是您如何优雅地处理任务取消:

处理“即发即弃”任务

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
} );

处理 await 任务完成/取消

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
于 2016-05-04T21:53:58.577 回答
10

C# 6.0 对此有一个解决方案..过滤异常

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);
}
于 2015-12-02T03:50:28.143 回答
10

你可以这样做:

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;
    }
}
于 2019-06-11T13:23:20.217 回答
0

根据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; 
   }
}
于 2017-10-08T02:41:06.240 回答
-3

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

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);

        }
    }
于 2012-09-28T05:39:09.390 回答