So far in this series, I’ve talked about how to request and detect cancellation, but for the next couple of posts I’ll be switching perspectives and discussing ways to respond to cancellation.
I know I’ve probably said this a half dozen times already, but it bears repeating: cancellation is cooperative. Your code will be provided a
CancellationToken, and it must do something with that
CancellationToken in order to be cancelable. Most of the time, this is just passing the
CancellationToken down to lower-level APIs, but if you want cancelable code at the lowest level, there are a couple of other options. The one we’re looking at today is polling.
How to Poll
The normal pattern is to periodically call
The example code above checks the cancellation token before it starts work, which is a good general practice. It is possible that the token is already cancelled by the time your operation starts running.
ThrowIfCancellationRequested will check to see if cancellation is requested, and if it is, it will throw
OperationCanceledException. So it handles the proper reporting of cancellation for you; your code should just let that exception propagate out of the method.
One question you’ll need to answer is how often to poll. There really isn’t a good answer for this; ideally you probably want to poll a few times a second, but when you’re talking about CPU-bound code running on potentially very different machines, it’s pretty much a guess at where in the code the
ThrowIfCancellationRequested should go. Just put it in the best place(s), run some tests to see if cancellation feels responsive enough, and that’s the best you can do.
How Not to Poll
There’s a sadly common antipattern regarding polling for cancellation, particularly in infinite loops: the “while not cancelled” loop, which looks like this:
When this code runs, it will periodically check the cancellation token; but when cancellation is requested, the method just returns early. This method doesn’t satisfy the cancellation contract of throwing an exception on cancellation. This means that the calling code cannot know whether the method ran to completion or whether it was cancelled.
The proper solution is to use
ThrowIfCancellationRequested, even for infinite loops:
When to Poll
Polling is an appropriate option for observing cancellation if your code is synchronous, such as CPU-bound code. Cancellation is often thought of as only a concern for asynchronous code; it was explicitly pointed out in the documentation for
async was introduced. But cancellation in .NET predates
async, and cancellation is just as applicable to synchronous code as to asynchronous code. In fact,
Parallel loops and PLINQ each have built-in support for cancellation:
WithCancellation for PLINQ.
That’s not to say you can’t use it for asynchronous code. It’s also appropriate to inject a
ThrowIfCancellationRequested in asynchronous code, if you are not sure whether other methods will respect their cancellation tokens. Remember, taking a
CancellationToken parameter means that the method may respect cancellation; it may also ignore the token and just ignore any cancellation requests. So your code may want to inject cancellation checks in-between “steps” of its operation. In this case,
ThrowIfCancellationRequested isn’t so much “polling” as it is an “occasional check”:
While you can sprinkle calls to
ThrowIfCancellationRequested throughout your code like this, I only do this when testing indicates that the code does not respect cancellation. In other words, I assume that
DoStep2Async will respect cancellation until proven otherwise by testing.
It is also appropriate to use
ThrowIfCancellationRequested at certain points where your code is about to do something expensive. Just adding a cancellation check there means your code won’t have to do the expensive work if it’s cancelled anyway.
Polling - implemented by periodic calls to
ThrowIfCancellationRequested - is one way to respond to cancellation requests. This is the common solution for synchronous, CPU-bound methods, and can also be used in a few other scenarios. Most asynchronous code does not use polling; we’ll cover that scenario next time!