Cancellation, Part 5: Registration
• CommentsLast time in this series I talked about how to respond to cancellation requests by polling for them. That’s a common approach for synchronous or CPU-bound code. In this post, I’m covering a pattern more common for asynchronous code: registration.
Registration is a way for your code to get a callback immediately when cancellation is requested. This callback can then perform some operation (often calling a different API) to cancel the asynchronous operation. Due to the multiple ways callbacks may be invoked, it’s generally recommended that cancellation callbacks should not throw exceptions.
Your callback should not throw exceptions.
How to Register
Your code can register a callback with any CancellationToken
by calling CancellationToken.Register
. This callback is invoked when (if) the cancellation token is cancelled. The Register
method returns a cancellation token registration, which is essentially just an IDisposable
.
Pretty much every asynchronous API already has cancellation support, so the example code below is somewhat contrived. The API in this example code provides a StartSomething()
method to start some asynchronous operation, a StopSomething()
method to cancel that operation, and a SomethingCompletedTask
property to detect when the operation completed. There are very few APIs like this in the .NET ecosystem, but you may run into a design similar to this if you’re wrapping code from another language that doesn’t have a promise-based async
system.
Note that callbacks might never be invoked! Tasks always complete (that is, they always should complete), but that doesn’t hold for cancellation tokens. There are some cancellation tokens that will never be cancelled, so they will never invoke their callbacks.
Your callback might never be called.
A Race Condition
What happens if a cancellation token is cancelled at approximately the same time the callback is registered? This situation is properly handled by a simple rule: if a callback is ever added to a cancellation token that is already canceled, then it is immediately and synchronously invoked.
Your callback might be called immediately on the same thread before Register
returns.
Cleanup Is Important!
The lifetime of cancellation tokens can vary greatly. Some cancellation tokens are used for short, individual operations. Other cancellation tokens are used for application shutdown. When writing your cancelable code, ensure that your code disposes of the registration; this will prevent resource leaks in your application.
The using var registration
in the example code above (repeated below) is one common way of handling cleanup: the registration is disposed once the asynchronous work completes.
Be sure to dispose your cancellation token registrations!
Sharp Corner: Synchronous Cancellation Callbacks
As discussed in requesting cancellation, token sources may be cancelled by calling the Cancel
method. It’s important to note that any registered callbacks are immediately (and synchronously) run by the Cancel
method before it returns. This can be a source of deadlocks or other unexpected behavior if your code is written assuming that callbacks are invoked after the Cancel
method. Specifically, callbacks shouldn’t perform any blocking operation.
Your callback is invoked synchronously in most cases.
This is awkward often enough that .NET 8.0 added a CancellationTokenSource.CancelAsync
method which invokes the cancellation callbacks on a thread pool thread. Technically, it immediately and synchronously transitions the cancellation token source to the canceled state, and then queues the callback invocations on a thread pool thread. The returned task completes when the callbacks have completed.
One more wrinkle, actually: it’s possible that CancelAsync
will return before the callbacks have completed if it’s already been called. As soon as the CancellationTokenSource
transitions to the canceled state, any future calls to Cancel
or CancelAsync
will return immediately, even if a previous call to CancelAsync
hasn’t finished running its callbacks yet. But you probably don’t need to worry about that; it’s just a side note.
Summary
This post has a bunch of scary warnings, but really, registering callbacks is the natural way to implement cancellation at the lowest levels. Polling is commonly used in sample code because it’s simpler, but registration allows your code to react immediately.
Don’t let the warnings dissuade you from using cancellation registrations! They’re more like guidelines for proper usage:
- Your callback should not throw exceptions.
- Your callback might never be called.
- Your callback might be called immediately on the same thread before
Register
returns. - Be sure to dispose your cancellation token registrations.
- Your callback is invoked synchronously in most cases.
If you follow these guidelines, you should be able to use cancellation token registrations successfully!