简体   繁体   English

优雅地处理任务取消

[英]Elegantly handle task cancellation

When using tasks for large/long running workloads that I need to be able to cancel I often use a template similar to this for the action the task executes:当我需要能够取消的大型/长时间运行的工作负载使用任务时,我经常使用与此类似的模板来执行任务:

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

The OperationCanceledException should not be logged as an error but must not be swallowed if the task is to transition into the cancelled state. OperationCanceledException不应被记录为错误,但如果任务要转换为取消状态,则不得吞下。 Any other exceptions do not need to be dealt with beyond the scope of this method.任何其他异常都不需要处理超出此方法的范围。

This always felt a bit clunky, and visual studio by default will break on the throw for OperationCanceledException (though I have 'break on User-unhandled' turned off now for OperationCanceledException because of my use of this pattern).这总是感觉有点笨拙,默认情况下,visual studio 会在抛出OperationCanceledException时中断(尽管由于我使用了这种模式,我现在为OperationCanceledException关闭了“用户未处理的中断”)。

Ideally I think I'd like to be able to do something like this:理想情况下,我想我希望能够做这样的事情:

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

ie have some sort of exclusion list applied to the catch but without language support that is not currently possible (@eric-lippert: c# vNext feature :)).即有某种排除列表应用于捕获但没有当前不可能的语言支持(@eric-lippert:c# vNext 功能:))。

Another way would be through a continuation:另一种方法是通过延续:

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
}

but I don't really like that as the exception technically could have more than a single inner exception and you don't have as much context while logging the exception as you would in the first example (if I was doing more than just logging it).但我真的不喜欢那样,因为从技术上讲,异常可能有多个内部异常,并且在记录异常时没有像第一个示例中那样多的上下文(如果我所做的不仅仅是记录它)。

I understand this is a bit of a question of style, but wondering if anyone has any better suggestions?我知道这有点风格问题,但想知道是否有人有更好的建议?

Do I just have to stick with example 1?我只需要坚持示例 1 吗?

So, what's the problem?所以有什么问题? Just throw away catch (OperationCanceledException) block, and set proper continuations:只需扔掉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 distinguishes cancellation and fault. TPL 区分取消和故障。 Hence, cancellation (ie throwing OperationCancelledException within task body) is not a fault .因此,取消(即在任务主体内抛出OperationCancelledException不是错误

The main point: do not handle exceptions within task body without re-throwing them.要点:不要在不重新抛出它们的情况下处理任务主体内的异常。

Here is how you elegantly handle Task cancellation:以下是您如何优雅地处理任务取消:

Handling "fire-and-forget" Tasks处理“即发即忘”的任务

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

Handling await Task completion / cancellation处理等待任务完成/取消

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

C# 6.0 has a solution for this.. Filtering exception 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);
}

You could do something like this:你可以这样做:

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

According to this MSDN blog post , you should catch OperationCanceledException , eg根据this MSDN blog post ,您应该捕获OperationCanceledException ,例如

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

If your cancelable method is in between other cancelable operations, you may need to perform clean up when canceled.如果您的可取消方法介于其他可取消操作之间,您可能需要在取消时执行清理。 When doing so, you can use the above catch block, but be sure to rethrow properly:这样做时,您可以使用上面的 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; 
   }
}

I am not entirely sure of what you are trying to achieve here but I think the following pattern might help我不完全确定您要在这里实现什么,但我认为以下模式可能会有所帮助

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

You might have observed that I have removed the throw statement from here.您可能已经注意到我从这里删除了 throw 语句。 This will not throw the exception but will simply ignore it.这不会抛出异常,而是会简单地忽略它。

Let me know if you intend to do something else.如果您打算做其他事情,请告诉我。

There is yet another way which is quite close to what you have exhibited in your code还有另一种方式与您在代码中展示的非常接近

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

        }
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM