Reputation: 26057
Microsoft recommends to handled cancelled task by using try/catch
with OperationCanceledException
. At the same time, it is possible to wrap an executed task with continuation using .ContinueWith()
which will swallow OperationCanceledException
and not throw. It looks as continuation is still handling exception internally, but doesn't bubble it up.
Given a cancellable task (takes in a cancellation token) on a hot execution path, would it be still advised to use try/catch(OperationCanceledException)
or continuation approach?
For example:
await Task.Delay(delayValue, cts.Token);
Could be handled via
try
{
await Task.Delay(delayValue, cts.Token);
}
catch(OperationCanceledException)
{
// token triggered cancellation
return;
}
or via
var task = await Task.Delay(delayValue, cts.Token).ContinueWith(t => t);
if (task.IsCancelled)
{
// token triggered cancellation
return;
}
Upvotes: 5
Views: 842
Reputation: 39017
Sticking strictly to the question rather than the motivations, I ran a benchmark with 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;
}
}
According to the results, the ContinueWith
method is 5x faster:
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 |
That said, you can go even faster by avoiding the jump to the threadpool for the continuation:
[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 |
Now we're 27x faster.
Of course, it begs the question whether saving 10µs is actually going to make a difference in your application. If it is, then you probably want to avoid async
altogether.
[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 |
Edit: I'll need to spend some time on that last benchmark because I'm really surprised that it allocates as much memory as the async version. I wonder if the compiler is already doing that optimization behind the scenes (there was some talk about adding that feature on .net core, but I'd be surprised it has already been ported to .net framework), or there could be something going on with BenchmarkDotNet.
Upvotes: 4
Reputation: 3567
Behind the scenes await
will split your code into a initiating task and a continuation task. Many awaits will result in many tasks. This is the big advantage of using async/await - you don't have to deal with this complexity. It is better not to mix them. Anyway, await is simple, you can't add too many options. ContinueWith
on the other hand has several options. Among them is the overload permitting you to add TaskContinuationOptions. Without knowing what your final purpose is, this lets you use both continuation and the exception:
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();
Upvotes: 0