A .NET Programmer’s Guide to CancellationToken

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
Cancellation Approach Summary and Language Examples

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 CancellationTokens 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 CancellationTokens.

Although CancellationTokens 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:

  1. Create a RandomWorker class.
  2. RandomWorker should have a DoWorkAsync method that executes some random work.
  3. 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.

From top to bottom, the words
As a Microsoft Gold Partner, Toptal is your elite network of Microsoft experts. Build high-performing teams with the experts you need—anywhere and exactly when you need them!

Further Reading on the Toptal Engineering Blog:


Like this post? Please share to your friends:
Leave a Reply

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: