Cancellation Types in C#

If you want to support cancellation in C#, you'll want to get familiar with the CancellationTokenSource and CancellationToken types.

If you want to support cancellation in C#, you'll want to get familiar with the CancellationTokenSource and CancellationToken types.

  • CancellationTokenSource: This class controls a single CancellationToken and is where you can kick off the request to cancel an operation.
  • CancellationToken: This structure indicates when cancellation has been requested.

Creating Cancellation Objects

Support for cancellation starts with a CancellationTokenSource. When a source is created, it automatically creates a corresponding CancellationToken and makes that token accessible via its Token property. To change the state of the token, you need to have a reference to the token source.

You might wonder, "Why does there need to be a CancellationTokenSource and CancellationToken? Couldn't the types just be combined into one?" That's because the responsibilities of requesting cancellation and indicating cancellation have been separated. The CancellationTokenSource is used to request a cancellation, whereas the CancellationToken only indicates when cancellation request has been made. This separation is useful in that it allows you to send a CancellationToken to parts of the code which might need to react to cancellation but that you wouldn't want to initiate a cancellation. This is a good example of Separation of Concerns.

Cancelling an Operation

There are two ways to request cancellation via the CancellationTokenSource: use its Cancel() method or provide a timeout to the constructor.

Cancel Method

The CancellationTokenSource has a method called Cancel which can be invoked to request a cancellation. When this method has been called, it will change the state of the corresponding CancellationToken to indicate that cancellation has been requested.

var tokenSource = new CancellationTokenSource();
// isCanceled will be false here.
bool isCanceled = tokenSource.Token.IsCancellationRequested;
// This line will request cancellation.
tokenSource.Cancel();
// isCanceled is now true.
isCanceled = tokenSource.Token.IsCancellationRequested;

Timeout

You can also provide a timeout period via the CancellationTokenSource's constructor to automatically request cancellation after the timeout period has elapsed.

var timeout = TimeSpan.FromSeconds(30);
// The associated token will be automatically canceled after 30 seconds.
var tokenSource = new CancellationTokenSource(timeout);

Disposing of CancellationTokenSource

CancellationTokenSource implements IDisposable and you should always dispose of your token sources when you're finished with them. If you don't you could create memory leaks.

// Wrap the token source in a using statement
// to automatically invoke its Dispose() method
// at the end of the using block.
using (var tokenSource = new CancellationTokenSource())
{
    // Perform an operation with the token here.
}

Checking for Cancellation

There are three ways to check the token for cancellation: use the IsCancellationRequested property, register a callback, or use the token's wait handle.

IsCancellationRequested

The CancellationToken has a boolean property named IsCancellationRequested that can be checked periodically (polled) to see if the operation should be cancelled.

Note that the CancellationToken is a struct, which means it can never be null. As a result, you don't have to check the token for nullability before reading the IsCancellationRequested property.

public async Task ProcessItemsAsync(IEnumerable<object> items, CancellationToken token)
{
    foreach (var item in items)
    {
        if (token.IsCancellationRequested)
            break;
        await ProcessItemAsync(item);
    }
}

Register

You can also register a callback method to be invoked when cancellation is requested.

public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
    cancellationToken.Register(() =>
    {
        // This block will be called as soon as
        // cancellation has been requested.
    });
    // Do other stuff in your method.
}

WaitHandle

The CancellationToken also exposes a WaitHandle that can be used to observe cancellation.

public void BlockAndWait(TimeSpan waitTime,
    CancellationToken cancellationToken)
{
    // Will block the thread for the specified time
    // or will stop blocking once cancellation is requested.
    cancellationToken.WaitHandle.WaitOne(waitTime);
    cancellationToken.ThrowIfCancellationRequested();
}

Throwing OperationCanceledException

It's often a good idea to throw an OperationCanceledException when you detect that cancellation has been requested. If you react to the cancellation request but don't throw an exception, then whoever is calling your code won't know if your code finished because it was actually complete or if it finished because the cancellation token told it to stop. If you throw an exception, then the caller can catch that exception and react to the cancellation.

If your code didn't receive a CancellationToken from the caller, then you don't want to throw an OperationCanceledException, as that would be unintuitive to the caller, since they didn't control or influence the cancellation.

You might argue that exceptions should be reserved for exceptional circumstances and in this case, the OperationCanceledException is used to control the flow of the program's logic. There's merit in that argument, but this is a pattern that is commonly used in C# to indicate that an operation has been canceled. Thus, consumers of your code are likely to be familiar with, if not expecting, this behavior.

If you want to throw an OperationCanceledException if cancellation was requested, then you can use the cancellation token's ThrowIfCancellationRequested() method. This method checks the IsCancellationRequested property and then throws a new OperationCanceledException if the value of the property is true. This check only happens when you invoke the ThrowIfCancellationRequested() method.

public async Task ProcessItemsAsync(IEnumerable<object> items, CancellationToken token)
{
    foreach (var item in items)
    {
        token.ThrowIfCancellationRequested();
        await ProcessItemAsync(item);
    }
}

Catching Cancellation Exceptions

It's common for cancelable operations to throw exceptions like OperationCanceledException or TaskCanceledException when they have been canceled. TaskCanceledException actually inherits from OperationCanceledException, so when you write your catch block, you'll probably want to stick with OperationCanceledException.

try
{
    await RunAsync(cancellationToken);
}
catch (OperationCanceledException)
{
    // Do something with the cancellation here.
}

Performance

There's a slight performance hit that you incur when checking IsCancellationRequested on the CancellationToken. In my very informal testing, I've found it to be around 33% slower than checking an ordinary property. To avoid a slight performance hit, you can check the CanBeCanceled property on the CancellationToken to see if you need to pay attention to cancellation requests. Do keep in mind, however, that this performance hit will be unnoticeable in the majority of scenarios.

public async Task RunAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.CanBeCanceled)
    {
        // Do logic to check for cancellation.
    }
}

Accepting Cancellation Tokens

When writing a method that accepts a CancellationToken it's expected that the token will be the last parameter that the method accepts. This is a convention that is followed throughout C#, so this should jive nicely with the expectations of your code's consumers.

Additionally, consider making the token an optional parameter, so that the caller of your method doesn't have to provide one if they don't have it.

public async Task RunAsync(IEnumerable<object> items,
    CancellationToken cancellationToken = default)
{
    // Do stuff here.
}