Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TaskCancellationException how to avoid the exception on success control flow?

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?

like image 295
quadroid Avatar asked Dec 15 '15 10:12

quadroid


1 Answers

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!

like image 63
Luaan Avatar answered Nov 15 '22 13:11

Luaan