.NET Cancellation Tokens

Introduction

Cancellation tokens in .NET were introduced as part of the .NET 4 Cancellation Framework. Essentially, it is a standard, cooperative, way to request for the cancellation of the execution of an operation. By cooperative it means that the code that uses it must abide to some rules, as opposed to the cancellation just stopping (killing) the execution.

One example of cancellation is, on a web app, when the client closes the connection while the server is performing some long operation. In cases such as this, we may want to abort the operation, as we are not sending the results anywhere (or not, as we shall see). Because of this, ASP.NET Core allows us to add a parameter of type CancellationToken to our asynchronous actions, it is automatically associated with the client and is therefore signalled when the client connection is closed.

CancellationToken is, essentially, behind the scenes, a ManualResetEvent. Throughout this article I will either be referring to a token being signalled or cancelled, it's exactly the same thing.

Creating Cancellation Tokens

To create a cancellation token (CancellationToken instance), we usually use a cancellation token source (CancellationTokenSource):

var cts = new CancellationTokenSource();
var ct = cts.Token;

The Token property returns the single token that was generated by this source, which never changes.

It is possible to create a CancellationToken directly, by calling its constructor, but, the initial state that you pass it there (cancelled or not) cannot be changed without a source.

In ASP.NET Core web applications, the cancellation token for the current request is always available as the HttpContext.RequestAborted property. This is the same instance that you receive as a parameter to action methods.

Getting a Cancellation Notification

There are two ways to tell if a cancellation token source (and its token) has bene cancelled:

  • By registering a notification callback
  • By polling it

We can register callbacks that will be called when the source is cancelled, through the Register method:

ct.Register(() => Console.WriteLine("Token was cancelled"));
ct.Register((state) => Console.WriteLine($"Token was cancelled with state {state}"), someState);
ct.Register((state, token) => Console.WriteLine($"Token was cancelled with state {state} and token {token}"), someState);

As you can see, some overloads here, essentially, you can pass an arbitrary object to the callback, and also the token itself.

One thing to always keep in mind is, the callback code should not throw any exception! Also, if the token is already cancelled, the callbacks will run immediately.

Now, each overload of Register returns a CancellationTokenRegistration:

using var registration = ct.Register(() => Console.WriteLine("Token was cancelled"));
...
registration.Unregister();

When we no longer want to receive notifications about a token, we just call its Unregister() method. Or, dispose of it, it results the same.

Cancelling a Token

Now, as we have seen, in the case of ASP.NET Core applications, the cancellation token that you pass as a parameter to an action method is signalled (cancelled) automatically when the client closes the connection (as per HttpContext.RequestAborted), but, of course, this can be done explicitly by one of two ways:

  1. Using a timer on the source
  2. Explicitly calling one of the cancel methods on the source

Bear in mind that these operations can only be executed on the cancellation token source, not on the token itself. The token merely signals if it was cancelled or not.

For the first case, we have the CancelAfter method, with two overloads:

var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));

For the latter, it's either the Cancel or the CancelAsync methods that we need to call:

var cts = new CancellationTokenSource()
...
cts.Cancel();
//or
await cts.CancelAsync();

There is overload on Cancel (not on CancelAsync) whick takes a throwOnFirstException boolean parameter (the default is false, if not supplied), which, if set to true, will throw an exception if any of the registered callbacks throw any exception.

A cancellation token source can only be cancelled once, meaning, if there are any registered callbacks, they will only fire once. Once it is cancelled, it cannot be un-cancelled.

Checking if a Token is Cancelled

You can check if a token (or a token source) has been triggered for cancellation by checking the IsCancellationRequested property (both classes offer an identical property):

var cts = new CancellationTokenSource()
var ct = cts.Token;
var isTokenCancellationRequested = ct.IsCancellationRequested;
var isTokenSourceCancellationRequested = cts.IsCancellationRequested;

The two properties, the one from CancellationTokenSource and the one from CancellationToken, will always return the same value.

Another option is to look at the inner WaitHandle

var isCancelled = ct.WaitHandle.WaitOne(0);

A timeout of 0 makes WaitOne return immediately either true, if the token is signalled for cancellation, or false, otherwise.

There is also another option: have the token throw an exception if it is cancelled:

ct.ThrowIfCancellationRequested();

The ThrowIfCancellationRequested call is functionally equivalent to:

if (ct.IsCancellationRequested) throw new OperationCanceledException(ct);

Remember that you should never throw an exception from inside a callback handler.

There is a property CanBeCanceled which checks if the token can be cancelled, but, unless its a Null cancellation token (more on this in a moment), or has already been cancelled, it should always return true.

Resetting a Token Source

Resetting a token source by no means signifies that it can transition from cancelled to not cancelled, that is not possible. What it means is, all callback registrations will be lost, and any cancellation timer that may have been set using CancelAfter will be cancelled too. The TryReset method on CancellationTokenSource will see if the token source can be cancelled, in which case it returns true, or false, otherwise.

var cts = new CancellationTokenSource()
cts.Cancel();
cts.TryReset(); //false

The Null Token

In the case when we need to have a CancellationToken just to pass along, but don't really need to ever cancel it, we can use the CancellationToken.None static read only instance:

var ct = CancellationToken.None;
var isCancellationRequested = ct.IsCancellationRequested; //always false
var canBeCancelled = ct.CanBeCanceled; //always false

As can be seen, both CanBeCanceled , and IsCancellationRequested will always return false.

Everythings works the usual way, it just never gets cancelled. This is an implementation of the Null Object Pattern, a nop.

Linked Token Sources

Now, suppose you have a cancellation token, but not its source, and you want to be able to signal it. As you may know by now, you can't, but what you can do is create a linked token source. This linked source can actually be used for different scenarios:

  • Create what can be considered a "child" token source, and its token, which will be cancelled along with its "parent"
  • Signal if any of multiple tokens is signalled

For example:

var cts1 = new CancellationTokenSource();
var ct1 = cts1.Token;
var cts2 = CancellationTokenSource.CreateLinkedTokenSource(ct1);
var ct2 = cts2.Token;
cts1.Cancel();
var cancelled1 = ct1.IsCancellationRequested; //true
var cancelled2 = ct2.IsCancellationRequested; //true

However, if we cancel instead the "child" token source, it's "parent" won't be cancelled

cts2.Cancel();
var cancelled1 = ct1.IsCancellationRequested; //false
var cancelled2 = ct2.IsCancellationRequested; //true

And if we want to wait for any of a series of tokens:

var cts1 = new CancellationTokenSource();
var ct1 = cts1.Token;
var cts2 = new CancellationTokenSource();
var ct2 = cts2.Token;
var cts3 = new CancellationTokenSource();
var ct3 = cts3.Token;
cts3.Cancel();
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct1, ct2, ct3);
var isCancelled = linkedCts.IsCancellationRequested; //true

The CreateLinkedTokenSource static method as a few overloads to accommodate these scenarios.

Using Cancellation Tokens

As we've seen, for ASP.NET Core web apps, a cancellation token can be injected automatically for us, and bound to the client connection:

public async Task<IActionResult> PerformLongOperation(CancellationToken ct) { ... }

Now, what we need to do is pass that token along to every asynchronous method that needs to be cancelled together with the client connection:

var data = await ctx.Data.ListAsync(ct);

Most asynchronous methods in .NET already have an overload that take a CancellationToken, so make sure you use them.

Now, a different thing is, what if some operations, once they're started, should not be cancelled? Well, for these cases, you should not pass them the CancellationToken, because we don't want our system to land in an inconsistent state:

var data = await ctx.Data.ListAsync(ct);
await PerformAtomicOperation(data); //look ma, no CancellationToken parameter

And what if we are in a loop and need to check periodically?

while (!ct.IsCancellationRequested)
{
    DoWork();
}

Or, if we prefer to throw exceptions:

while (true)
{
    ct.ThrowIfCancellationRequested();
    DoWork();
}

Some APIs, such as Tasks, have built-in methods for working with CancellationTokens. For example, TaskFactory:

CancellationToken ct = ...;
var task = Task.Factory.StartNew((state) =>
{
    DoWork((CancellationToken)state);
}, ct);

In some cases, however, you may want to start a new cancellation token, by calling CreateLinkedTokenSource. This is when you still want to be notified when the "parent" operation has been signalled for cancelling, but, you want to create a new scope for new operations, which can also be cancelled without affecting the "parent" scope.

Cancelling Other Tokens Automatically

We may want to automatically signal a different token when our was signalled. We can achieve this easily with registrations:

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
var ct1 = cts1.Token;
var ct2 = cts2.Token;
using var registration = ct2.Register(cts1.Cancel);

Releasing Tokens and Token Sources

Because CancellationTokenSource implements IDisposable, you should dispose of it, either manually or in a using block. After it has been disposed, you should not hold any references to it, as it can no longer be used. The CancellationToken class does not need to be disposed.
using var cts = new CancellationTokenSource();

Conclusion

I hope you understood the importance of cancellation tokens. Make sure you use them according to the best practices - pass them along any asynchronous methods unless you don't want some operation to be cancelled after a point, dispose of its resources, do not throw exceptions from callbacks - and you will have a powerful, yet simple, framework for dealing with cancellation requests.

Comments

Popular posts from this blog

Audit Trails in EF Core

Domain Events with .NET