Cancellation Types in C#
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.
}