Sometimes canceling is a good thing. In many of my .NET projects, I have had plenty of motivation to cancel both internal and external processes. Microsoft learned that developers were approaching this common use case in a variety of complex implementations and decided there must be a better way. Thus, a common cancellation communication pattern was introduced as CancellationToken
, which was built using lower-level multithreading and interprocess communication constructs. As part of my initial research into this pattern—and after having dug through the actual .NET source code for Microsoft’s implementation—I found that CancellationToken
can solve a much broader set of problems: subscriptions on applications’ run states, timing out operations using different triggers, and general interprocess communications via flags.
The Intended CancellationToken Use Case
CancellationToken
was introduced in .NET 4 as a means to enhance and standardize the existing solutions for canceling operations. There are four general approaches to handling cancellation that popular programming languages tend to implement:
Kill | Tell, don’t take no for an answer | Ask politely, and accept rejection | Set flag politely, let it poll if it wants | |
---|---|---|---|---|
Approach | Hard stop; resolve inconsistencies later | Tell it to stop but let it clean things up | A direct but gentle request to stop | Ask it to stop, but don’t force it |
Summary | A surefire path to corruption and pain | Allows clean stop points but it must stop | Allows clean stop points, but the cancellation request may be ignored | Cancellation is requested through a flag |
Pthreads |
pthread_kill ,pthread_cancel (async) |
pthread_cancel (deferred mode) |
n/a | Through a flag |
.NET | Thread.Abort |
n/a | Thread.Interrupt |
Through a flag in CancellationToken
|
Java |
Thread.destroy ,Thread.stop
|
n/a | Thread.interrupt |
Through a flag or Thread.interrupted
|
Python | PyThreadState_SetAsyncExc |
n/a | asyncio.Task.cancel |
Through a flag |
Guidance | Unacceptable; avoid this approach | Acceptable, especially when a language doesn’t support exceptions or unwinding | Acceptable if the language supports it | Better, but more of a group effort |
CancellationToken
resides in the final category, where the cancellation conversation is cooperative.
After Microsoft introduced CancellationToken
, the development community quickly embraced it, particularly because many major .NET APIs were updated to use these tokens natively. For example, beginning with ASP.NET Core 2.0, actions support an optional CancellationToken
parameter that may signal if an HTTP request has been closed, allowing cancellation of any operation and thus avoiding needless use of resources.
After a deep dive into the .NET codebase, it became clear that CancellationToken
’s usage is not limited to cancellation.
CancellationToken Under a Microscope
When looking more closely at CancellationToken
’s implementation, we see it’s just a simple flag (i.e., ManualResetEvent
) and the supporting infrastructure that provides the ability to monitor and change that flag. CancellationToken
’s main utility is in its name, which suggests this is the common way to cancel operations. Nowadays, any .NET library, package, or framework with asynchronous or long-running operations allows cancellation through these tokens.
CancellationToken
may be triggered either by manually setting its flag to “true” or programming it to change to “true” after a certain time span has elapsed. Regardless of how a CancellationToken
is triggered, client code that is monitoring this token may determine the token flag’s value through one of three methods:
- Using a
WaitHandle
- Polling the
CancellationToken
’s flag - Informing the client code when the flag’s state is updated through a programmatic subscription
After further research in the .NET codebase, it became evident that the .NET team found CancellationTokens
useful in other scenarios not connected to cancellation. Let’s explore some of these advanced and off-brand use cases, which empower C# developers with multithreaded and interprocess coordination to simplify complex situations.
CancellationTokens for Advanced Events
When writing ASP.NET Core applications, we sometimes need to know when our application has started, or we need to inject our code into the host shutdown process. In those cases, we use the IHostApplicationLifetime
interface (previously IApplicationLifetime
). This interface (from .NET Core’s repository) makes use of CancellationToken
to communicate three major events: ApplicationStarted
, ApplicationStopping
, and ApplicationStopped
:
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Allows consumers to be notified of application lifetime events.
/// This interface is not intended to be user-replaceable.
/// </summary>
public interface IHostApplicationLifetime
{
/// <summary>
/// Triggered when the application host has fully started.
/// </summary>
CancellationToken ApplicationStarted { get; }
/// <summary>
/// Triggered when the application host is starting a graceful shutdown.
/// Shutdown will block until all callbacks registered on
/// this token have completed.
/// </summary>
CancellationToken ApplicationStopping { get; }
/// <summary>
/// Triggered when the application host has completed a graceful shutdown.
/// The application will not exit until all callbacks registered on
/// this token have completed.
/// </summary>
CancellationToken ApplicationStopped { get; }
/// <summary>
/// Requests termination of the current application.
/// </summary>
void StopApplication();
}
}
At first glance, it may seem like CancellationToken
s don’t belong here, especially since they are being used as events. However, further examination reveals these tokens to be a perfect fit:
- They are flexible, allowing for multiple ways for the interface’s client to listen to these events.
- They are thread-safe out of the box.
- They can be created from different sources by combining
CancellationToken
s.
Although CancellationToken
s aren’t perfect for every event need, they are ideal for events that happen only once, like application start or stop.
CancellationToken for Timeout
By default, ASP.NET gives us very little time in which to shut down. In those cases where we want a little more time, using the built-in HostOptions
class allows us to change this timeout value. Underneath, this timeout value is wrapped in a CancellationToken
and fed into the underlying subprocesses.
IHostedService
’s StopAsync
method is a great example of this usage:
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Defines methods for objects that are managed by the host.
/// </summary>
public interface IHostedService
{
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start
/// process has been aborted.</param>
Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown
/// process should no longer be graceful.</param>
Task StopAsync(CancellationToken cancellationToken);
}
}
As evident in the IHostedService
interface definition, the StopAsync
method takes one CancellationToken
parameter. The comment associated with that parameter clearly communicates Microsoft’s initial intent for CancellationToken
was as a timeout mechanism rather than a cancellation process.
In my opinion, if this interface had existed prior to CancellationToken
’s existence, this could have been a TimeSpan
parameter—to indicate how long the stop operation was allowed to process. In my experience, timeout scenarios can almost always be converted to a CancellationToken
with great additional utility.
For the moment, let’s forget that we know how the StopAsync
method is designed and instead think about how we would design this method’s contract. First let’s define the requirements:
- The
StopAsync
method must try to stop the service. - The
StopAsync
method should have a graceful stop state. - Regardless of whether a graceful stop state is achieved, a hosted service must have a maximum time in which to stop, as defined by our timeout parameter.
By having a StopAsync
method in any form, we satisfy the first requirement. The remaining requirements are tricky. CancellationToken
satisfies these requirements exactly by using a standard .NET flag-based communication tool to empower the conversation.
CancellationToken As a Notification Mechanism
The biggest secret behind CancellationToken
is that it’s just a flag. Let’s illustrate how CancellationToken
can be used to start processes instead of stopping them.
Consider the following:
- Create a
RandomWorker
class. -
RandomWorker
should have aDoWorkAsync
method that executes some random work. - The
DoWorkAsync
method must allow a caller to specify when the work should begin.
public class RandomWorker
{
public RandomWorker(int id)
{
Id = id;
}
public int Id { get; }
public async Task DoWorkAsync()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Worker {Id}] Iteration {i}");
await Task.Delay(1000);
}
}
}
The above class satisfies the first two requirements, leaving us with the third. There are several alternate interfaces we could use to trigger our worker, like a time span or a simple flag:
# With a time span
Task DoWorkAsync(TimeSpan startAfter);
# Or a simple flag
bool ShouldStart { get; set; }
Task DoWorkAsync();
These two approaches are fine, but nothing is as elegant as using a CancellationToken
:
public class RandomWorker
{
public RandomWorker(int id)
{
Id = id;
}
public int Id { get; }
public async Task DoWorkAsync(CancellationToken startToken)
{
startToken.WaitHandle.WaitOne();
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Worker {Id}] Iteration {i}");
await Task.Delay(1000);
}
}
}
This sample client code illustrates the power of this design:
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CancelToStart
{
public class Program
{
static void Main(string[] args)
{
CancellationTokenSource startCts = new CancellationTokenSource();
startCts.CancelAfter(TimeSpan.FromSeconds(10));
var tasks = Enumerable.Range(0, 10)
.Select(i => new RandomWorker(i))
.Select(worker => worker.DoWorkAsync(startCts.Token))
.ToArray();
Task.WaitAll(tasks, CancellationToken.None);
}
}
}
The CancellationTokenSource
will create our CancellationToken
behind the scenes and coordinate the triggering of all the associated processes. In this case, the associated process is our RandomWorker
, which is waiting to start. This approach allows us to leverage the thread safety baked into the default CancellationToken
implementation.
These examples demonstrate how CancellationToken
provides a toolbox of solutions that are useful outside of its intended use case. The tools can come in handy in many scenarios that involve interprocess flag-based communication. Whether we are faced with timeouts, notifications, or one-time events, we can fall back on this elegant, Microsoft-tested implementation.