优雅地处理任务取消

Eam*_*mon 31 c# exception-handling task-parallel-library

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

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

不应将OperationCanceledException记录为错误,但如果任务要转换为已取消状态,则不得吞下OperationCanceledException.除了此方法的范围之外,不需要处理任何其他异常.

这总觉得有点笨重,默认情况下visual studio会在OperationCanceledException的中断(虽然因为我使用了这种模式,我已经'关闭了User-unhandled'现在关闭了OperationCanceledException).

理想情况下,我认为我希望能够做到这样的事情:

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

即将某种排除列表应用于捕获但没有当前不可能的语言支持(@ eric-lippert:c#vNext feature :)).

另一种方式是通过延续:

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

但我真的不喜欢这样,因为异常在技术上可能只有一个内部异常而且你在记录异常时没有像第一个例子那样多的上下文(如果我做的不仅仅是记录它).

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

我只需坚持示例1吗?

埃蒙

Den*_*nis 15

所以有什么问题?扔掉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();
Run Code Online (Sandbox Code Playgroud)

TPL区分取消和错误.因此,取消(即OperationCancelledException在任务主体内投掷)不是一个错误.

要点:不要在没有重新抛出它们的情况下处理任务体内的异常.


Cas*_*son 9

以下是优雅处理任务取消的方法:

处理"即发即忘"的任务

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

处理等待任务完成/取消

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


Jes*_*yer 8

你可以这样做:

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

  • @OhadSchneider 一般来说(不是在特定的简单示例中)不能保证“cancelToken”将通过“OperationCanceledException.CancellationToken”传播。链接令牌通常由异步方法在内部创建,隐藏原始“CancellationToken”的身份。恕我直言,在大多数情况下,检查条件“when (cancelToken.IsCancellationRequested)”是最好的选择。 (3认同)

小智 7

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

  • 关键字实际上是'何时'而非'如果'.语法(对于OP)将是:catch(Exception ex)when(!(ex是OperationCanceledException)) (8认同)