In our application we work a lot with async / await and Tasks. Therefore it does use Task.Run a lot, sometimes with cancellation support using the built in CancellationToken
.
public Task DoSomethingAsync(CancellationToken cancellationToken)
{
return Task.Run(() =>
{
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
//do some work
}
}, cancellationToken);
}
If i do now cancel the execution using the CancellationToken the execution does stop at the beginning of the next loop, or if the Task did not start at all it throws an exception (TaskCanceledException inside Task.Run). The question is now why does Task.Run use an Exception to control successful cancellation instead of just returning a completed Task. Is there any specific reason MS did not stick to the "Do NOT use exceptions to control execution flow" rule?.
And how can i avoid Boxing every method that supports cancellation (which are a lot) in an completely useless try catch (TaskCancelledException) block?
Well, you can't really see the difference in your very simple scenario - you're not actually using the result of the Task
, and you don't need to propagate the cancellation through a complex call stack.
First, your Task
might return a value. What do you return when the operation was cancelled?
Second, there may be other tasks that follow your cancelled task. You probably want to propagate the cancellation through the other tasks at your convenience.
Exceptions propagate. Task cancellation is pretty much identical to Thread.Abort
in this usage - when you issue a Thread.Abort
, a ThreadAbortException
is used to make sure you unwind all the way back to the top. Otherwise, all of your methods would have to check the result of every method they call, check if they were cancelled, and return themselves if needed - and we've already seen that people will ignore error return values in old-school C :)
In the end, task cancellation, just like thread aborts, is an exceptional scenario. It already involves synchronization, stack unwinding etc.
However, this doesn't mean you necessarily have to use try-catch
to catch the exception - you can use task states. For example, you can use a helper function like this:
public static Task<T> DefaultIfCanceled<T>(this Task<T> @this, T defaultValue = default(T))
{
return
@this.ContinueWith
(
t =>
{
if (t.IsCanceled) return defaultValue;
return t.Result;
}
);
}
Which you can use as
await SomeAsync().DefaultIfCanceled();
Of course, it should be noted that noöne is forcing you to use this method of cancellation - it's simply provided as a convenience. For example, you could use your own amplified type to preserve the cancellation information, and handle the cancellation manually. But when you start doing that, you'll find the reason why cancellation is handled using exceptions - doing this in imperative code is a pain, so you'll either waste a lot of effort for no gain, or you'll switch to a more functional way of programming (come, we have cookies!*).
(*) Disclaimer: We don't actually have cookies. But you can make your own!
The exception is thrown for a purpose as others in the community have already pointed it out.
However, if you would like to have more control over TaskCanceledException
behaviour and still have the logic isolated to one place you may implement an Extension method to extend Task
which handles cancellation, something like this -
public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
await Task.Run(() =>
{
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
//do some work
}
}).
WithCancellation(cancellationToken,false); // pass the cancellation token to extension funciton instead to run
}
static class TaskCacellationHelper
{
private struct Void { } // just to support TaskCompletionSource class.
public static async Task WithCancellation(this Task originalTask, CancellationToken ct, bool suppressCancellationExcetion)
{
// Create a Task that completes when the CancellationToken is canceled
var cancelTask = new TaskCompletionSource<Void>();
// When the CancellationToken is canceled, complete the Task
using (ct.Register(
t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask))
{
// Create a Task that completes when either the original or
// CancellationToken Task completes
Task any = await Task.WhenAny(originalTask, cancelTask.Task);
// If any Task completes due to CancellationToken, throw OperationCanceledException
if (any == cancelTask.Task)
{
//
if (suppressCancellationExcetion == false)
{
ct.ThrowIfCancellationRequested();
}
else
{
Console.WriteLine("Cancelled but exception supressed");
}
}
}
// await original task. Incase of cancellation your logic will break the while loop
await originalTask;
}
}