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:
| Approach | 1) Kill | 2) Tell, don’t take no for an answer | 3) Ask politely, and accept rejection | 4) Set flag politely, let it poll if it wants |
|---|---|---|---|---|
| Description | 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 |
| Analysis | 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 |
| 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 |
| pthreads implementation | pthread_kill,pthread_cancel (async) | pthread_cancel (deferred mode) | n/a | Through a flag |
| .NET implementation | Thread.Abort | n/a | Thread.Interrupt | Through a flag in CancellationToken |
| Java implementation | Thread.destroy,Thread.stop | n/a | Thread.interrupt | Through a flag or Thread.interrupted |
| Python implementation | PyThreadState_SetAsyncExc | n/a | asyncio.Task.cancel | Through a flag |
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:
| |
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:
| |
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
StopAsyncmethod 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:
- Create a
RandomWorkerclass. - This class should have a
DoWorkAsyncmethod executing some arbitrary work. - The
DoWorkAsyncmethod should allow the caller to dictate when the work commences.
| |
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:
| |
While functional, these approaches lack the elegance of using a CancellationToken:
| |
This sample client code demonstrates the effectiveness of this design:
| |
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.
