Sunday 20 December 2015

How to cancel a Task


It is a good practice to offer users a chance to cancel some long-running operation. Such operation usually has to run asynchronously and in a TPL-based design is modeled as a method returning a Task.

Cancellation model requires some flag which is accessible by the caller and from inside the operation. Caller sets the flag when wants operation to be cancelled. Operation regularly checks the flag or is subscribed to the event "flag has been set" and if flag has been set, operation will stop doing the work. This model is in .NET abstracted with two types: CancellationTokenSource class and CancellationToken structure. Caller uses CancellationTokenSource to initiate cancellation and operation uses CancellationToken to check whether it has been cancelled.

When designing an API, if provision for operation cancellation is required, we have to add a CancellationToken to a list of method's arguments. API caller invokes CancellationTokenSource.Cancel method which sets CancellationToken.IsCancellationRequested to true. Target method checks this property and can either silently return or throw OperationCanceledException. The latter approach is better as only in this case returning task will come to Canceled state.

The following code depicts the whole process of task cancellation:

CancellationToken.ThrowIfCancellationRequested() method simply checks CancellationToken.IsCancellationRequested and if it's true, it throws OperationCanceledException. The output of the code above is:


.........System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowIfCancellationRequested()
   at MyService.d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.d__1.MoveNext()
Task.IsCanceled: True
Task.IsFaulted: False
Task.Exception: null


The following code shows that the await-ed task does not get aware that the task it holds got canceled if method silently returns on cancellation:

Output:

..........Task.IsCanceled: False
Task.IsFaulted: False
Task.Exception: null


Further reading:
Andrew Arnott - Recommended patterns for CancellationToken
MSDN: Task Cancellation

No comments: