Web Front-end7 minute read

A .NET Programmer’s Guide to CancellationToken

Microsoft created a standardized cancellation implementation that has far-reaching capabilities beyond its original use case.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Microsoft created a standardized cancellation implementation that has far-reaching capabilities beyond its original use case.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Davit Asryan
Verified Expert in Engineering

Davit is a software engineer who has a broad range of experience in creating enterprise software for companies, including Veritas. His expertise is developing with .NET and C#.

Read More

Expertise

PREVIOUSLY AT

Veritas
Share

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:

 KillTell, don’t take no for an answerAsk politely, and accept rejectionSet flag politely, let it poll if it wants
ApproachHard stop; resolve inconsistencies laterTell it to stop but let it clean things upA direct but gentle request to stopAsk it to stop, but don’t force it
SummaryA surefire path to corruption and painAllows clean stop points but it must stopAllows clean stop points, but the cancellation request may be ignoredCancellation is requested through a flag
Pthreads pthread_kill,
pthread_cancel (async)
pthread_cancel (deferred mode)n/aThrough a flag
.NETThread.Abortn/aThread.InterruptThrough a flag in CancellationToken
Java Thread.destroy,
Thread.stop
n/aThread.interruptThrough a flag or Thread.interrupted
PythonPyThreadState_SetAsyncExcn/aasyncio.Task.cancelThrough a flag
GuidanceUnacceptable; avoid this approachAcceptable, especially when a language doesn’t support exceptions or unwindingAcceptable if the language supports itBetter, 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.

An Expansive CancellationToken Toolbox

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 "Gold" (colored gold), "Microsoft," and "Partner" (both in black) appear followed by the Microsoft logo.

Understanding the basics

  • What is CancellationToken?

    CancellationToken is a native, lightweight .NET construct used for enabling cooperative cancellation.

  • Can you reuse CancellationTokens?

    Yes, you can reuse CancellationTokens. A CancellationTokenSource is used to cancel a set of processes. All processes associated with a particular CancellationTokenSource will use one CancellationToken among them.

  • How do you handle a CancellationToken?

    Since cancellations are cooperative, CancellationToken handling is left to the method it is passed to. Generally, we either pass the CancellationToken to other methods or stop as soon as possible once the token is canceled.

  • How can I get a CancellationToken?

    A CancellationToken is created by the CancellationTokenSource. Only the CancellationTokenSource that creates the token can cancel it.

Hire a Toptal expert on this topic.
Hire Now
Davit Asryan

Davit Asryan

Verified Expert in Engineering

Yerevan, Armenia

Member since March 3, 2022

About the author

Davit is a software engineer who has a broad range of experience in creating enterprise software for companies, including Veritas. His expertise is developing with .NET and C#.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

Veritas

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.