try / catch(OperationCanceledException)vs ContinueWith()

时间:2018-08-20 01:19:46

标签: c# asynchronous async-await

Microsoft recommends通过将try/catchOperationCanceledException一起使用来处理已取消的任务。同时,可以使用.ContinueWith()将执行的任务继续包装,这将吞下OperationCanceledException而不抛出。看起来,延续仍在内部处理异常,但不会冒泡。

考虑到热执行路径上的可取消任务(采用取消令牌),是否仍建议使用try/catch(OperationCanceledException)或延续方法?

例如:

await Task.Delay(delayValue, cts.Token);

可以通过

处理
try
{
  await Task.Delay(delayValue, cts.Token);
}
catch(OperationCanceledException) 
{
  // token triggered cancellation
  return;
}

或通过

var task = await Task.Delay(delayValue, cts.Token).ContinueWith(t => t);
if (task.IsCancelled)
{
  // token triggered cancellation
  return;
}

2 个答案:

答案 0 :(得分:1)

严格关注问题而不是动机,我使用BenchmarkDotNet进行了基准测试:

[MemoryDiagnoser]
public class CancelBench
{
    private int delayValue = 15;

    private CancellationToken cancellationToken = new CancellationToken(true);

    [Benchmark]
    public async Task<bool> Exception()
    {
        try
        {
            await Task.Delay(delayValue, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            // token triggered cancellation
            return true;
        }
        return false;
    }

    [Benchmark]
    public async Task<bool> ContinueWith()
    {
        var task = await Task.Delay(delayValue, cancellationToken).ContinueWith(t => t);
        if (task.IsCanceled)
        {
            // token triggered cancellation
            return true;
        }
        return false;
    }
}

根据结果,ContinueWith方法快5倍:

                    Method |        Mean |      Error |    StdDev |  Gen 0 |  Gen 1 |  Gen 2 | Allocated |
-------------------------- |------------:|-----------:|----------:|-------:|-------:|-------:|----------:|
                 Exception | 13,823.6 ns | 105.359 ns | 93.398 ns | 0.0916 |      - |      - |     496 B |
              ContinueWith |  2,843.0 ns |  14.300 ns | 13.376 ns | 0.0496 | 0.0076 | 0.0038 |     276 B |

也就是说,通过避免跳转到线程池进行继续操作,您可以甚至更快:

    [Benchmark]
    public async Task<bool> ContinueWithSynchronously()
    {
        var task = await Task.Delay(delayValue, cancellationToken).ContinueWith(t => t, TaskContinuationOptions.ExecuteSynchronously);
        if (task.IsCanceled)
        {
            // token triggered cancellation
            return true;
        }
        return false;
    }
 ContinueWithSynchronously |    505.2 ns |   2.581 ns |  2.414 ns | 0.0277 |      - |      - |     148 B |

现在我们快了27倍。

当然,这引出了一个问题,即节省10µs是否实际上会对您的应用产生影响。如果是这样,那么您可能希望完全避免使用async

    [Benchmark]
    public Task<bool> NoAsync()
    {
        return Task.Delay(delayValue, cancellationToken).ContinueWith(t => t.IsCanceled, TaskContinuationOptions.ExecuteSynchronously);
    }
                   NoAsync |    397.7 ns |  5.290 ns |  4.948 ns | 0.0281 |      - |      - |     148 B |

编辑:我需要花一些时间在最后一个基准上,因为我真的感到惊讶,它分配的内存与异步版本一样多。我想知道编译器是否已经在幕后进行了优化(有人谈论在.net核心上添加该功能,但是我很惊讶它已经被移植到.net框架中了),或者可能会发生某些事情使用BenchmarkDotNet。

答案 1 :(得分:0)

await在幕后会将您的代码分为启动任务和继续任务。许多等待将导致许多任务。这是使用异步/等待的最大优势-您不必处理这种复杂性。最好不要混合它们。无论如何,等待很简单,您不能添加太多选项。另一方面,ContinueWith有几个选项。其中包括允许您添加TaskContinuationOptions的重载。在不知道最终目的是什么的情况下,您可以同时使用延续和异常:

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));

try
{
    await Task.Delay(TimeSpan.FromSeconds(5), cts.Token)
        .ContinueWith(t => Console.WriteLine("Continued"), TaskContinuationOptions.NotOnCanceled);
    Console.WriteLine("Hihi...");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled");
}
Console.ReadLine();