CancellationToken: A Comprehensive Guide for .NET Programmers

At times, canceling proves to be advantageous. In numerous .NET projects I’ve worked on, there were compelling reasons to halt both internal and external processes. Recognizing that developers were tackling this frequent scenario with diverse and intricate solutions, Microsoft sought a superior approach. Consequently, a standardized cancellation communication pattern was introduced as CancellationToken, constructed using low-level multithreading and interprocess communication mechanisms. During my preliminary exploration of this pattern, delving into the .NET source code of Microsoft’s implementation, I discovered that CancellationToken possesses versatility beyond cancellation: managing application run state subscriptions, implementing timeouts based on various triggers, and facilitating general interprocess communication through flags. While these applications of CancellationToken might not be universally relevant, they prove beneficial in specific complex situations.

The Primary Use Case of CancellationToken

Introduced in .NET 4, CancellationToken aimed to improve and standardize existing solutions for operation cancellation. Programming languages typically implement four approaches for cancellation:

Approach1) Kill2) Tell, don’t take no for an answer3) Ask politely, and accept rejection4) Set flag politely, let it poll if it wants
DescriptionHard 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
AnalysisA 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
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
pthreads implementationpthread_kill,
pthread_cancel (async)
pthread_cancel (deferred mode)n/aThrough a flag
.NET implementationThread.Abortn/aThread.InterruptThrough a flag in CancellationToken
Java implementationThread.destroy,
Thread.stop
n/aThread.interruptThrough a flag or Thread.interrupted
Python implementationPyThreadState_SetAsyncExcn/aasyncio.Task.cancelThrough a flag
Cancellation Approach Summary and Language Examples

CancellationToken aligns with the final category, emphasizing cooperative cancellation.

The development community readily adopted CancellationToken after its introduction, particularly due to its integration into major .NET APIs. A notable example is ASP.NET Core 2.0 onward, where actions can accept an optional CancellationToken parameter. This parameter signals HTTP request closures, enabling operation cancellation and preventing unnecessary resource consumption.

A thorough examination of the .NET codebase revealed that CancellationToken’s utility extends beyond cancellation.

Examining CancellationToken

A closer look at CancellationToken’s implementation reveals a simple flag (ManualResetEvent) and supporting infrastructure for monitoring and modifying it. The name itself highlights its primary purpose: a common mechanism for canceling operations. Currently, .NET libraries, packages, and frameworks with asynchronous or long-running operations generally utilize these tokens for cancellation.

Triggering a CancellationToken can be done manually by setting its flag to “true” or programmatically after a specific duration. Regardless of the trigger method, client code monitoring the token can determine its flag value through:

  • Employing a WaitHandle
  • Regularly checking the CancellationToken’s flag
  • Receiving programmatic notifications upon flag state changes

Further investigation of the .NET codebase revealed the .NET team leveraging CancellationTokens in scenarios unrelated to cancellation, highlighting advanced and unconventional applications. Let’s explore how these applications empower C# developers with multithreaded and interprocess coordination, simplifying complex situations.

Advanced Events with CancellationTokens

When developing ASP.NET Core applications, situations arise where we need to detect application startup or intervene during the host shutdown process. The IHostApplicationLifetime interface (formerly IApplicationLifetime) interface comes into play here. Originating from .NET Core’s repository, this interface utilizes CancellationToken to communicate three key events: ApplicationStarted, ApplicationStopping, and ApplicationStopped:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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, using CancellationTokens for events might seem unusual. However, upon closer examination, their suitability becomes evident:

  • They offer flexibility, enabling various ways for clients of the interface to listen for these events.
  • Thread safety is inherent in their design.
  • They allow creation from multiple sources by combining CancellationTokens.

While not ideal for every event scenario, CancellationTokens excel with events occurring only once, such as application start or stop.

CancellationToken for Timeout Implementation

ASP.NET’s default shutdown time is quite limited. To extend this, the HostOptions class provides a way to modify the timeout value. Internally, this value is encapsulated within a CancellationToken and passed to underlying subprocesses.

The StopAsync method of IHostedService exemplifies this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
    }
}

The IHostedService interface definition shows that the StopAsync method accepts a CancellationToken parameter. The associated comment clarifies that Microsoft initially intended CancellationToken for timeout mechanisms rather than solely for cancellation.

Hypothetically, if this interface predated CancellationToken, a TimeSpan parameter might have been used, specifying the maximum duration for the stop operation. My experience suggests that timeout scenarios can often be effectively translated to utilize a CancellationToken, unlocking additional benefits.

Disregarding our knowledge of the StopAsync method’s design, let’s consider designing its contract. The requirements are:

  • The StopAsync method should attempt to stop the service.
  • It should have a graceful stop state.
  • Regardless of achieving a graceful stop, a hosted service must adhere to a maximum stop time, defined by our timeout parameter.

The existence of a StopAsync method fulfills the first requirement. The remaining requirements are more intricate, and CancellationToken elegantly addresses them by using a standard .NET flag-based communication tool to facilitate the interaction.

CancellationToken as a Notification Mechanism

The key takeaway is that CancellationToken is fundamentally a flag. Let’s illustrate how it can initiate processes rather than solely stopping them.

Consider the following:

  1. Create a RandomWorker class.
  2. This class should have a DoWorkAsync method executing some arbitrary work.
  3. The DoWorkAsync method should allow the caller to dictate when the work commences.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 class above fulfills the first two requirements, leaving the third. Several alternative interfaces could trigger our worker, such as a time span or a simple flag:

1
2
3
4
5
6
# With a time span
Task DoWorkAsync(TimeSpan startAfter);

# Or a simple flag
bool ShouldStart { get; set; }
Task DoWorkAsync();

While functional, these approaches lack the elegance of using a CancellationToken:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 demonstrates the effectiveness of this design:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 handles the creation of our CancellationToken and coordinates the triggering of all associated processes, in this case, the waiting RandomWorker. This approach leverages the inherent thread safety of the default CancellationToken implementation.

A Versatile Toolkit: CancellationToken

These examples highlight how CancellationToken, beyond its intended purpose, offers a collection of solutions valuable for various interprocess flag-based communication scenarios. Whether dealing with timeouts, notifications, or one-time events, we can rely 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.
Licensed under CC BY-NC-SA 4.0