Cancellation, Part 6: Linking
• CommentsSo far we’ve covered how cancellation is requested by one piece of code, and responded to by another piece of code. The requesting code has a standard way of requesting cancellation, as well as a standard way of detecting whether the code was canceled or not. Meanwhile, the responding code can observe cancellation either by polling or (more commonly) by registering a cancellation callback. So far, so good; and we’re ready for the next step!
In this article, we’ll look at how linked cancellation tokens work.
Linked Cancellation Tokens
Linked cancellation tokens allow your code to create a CancellationTokenSource
that cancels whenever any other cancellation tokens are canceled, in addition to a manual cancellation request.
The following code creates a linked cancellation token source:
The DoSomethingAsync
method above takes a cancellationToken
- I’ll call this the “outer” cancellation token. It then creates a CTS that is linked to that outer token. Then, when it calls DoSomethingElseAsync
, it passes the token from that linked CTS, which I’ll call the “inner” cancellation token.
If the outer cancellation token (cancellationToken
) is ever canceled, then the linked CTS (cts
) and its inner cancellation token (cts.Token
) are also canceled. Furthermore, the DoSomethingAsync
method has the option of explicitly cancelling the linked CTS - in this case, only the inner cancellation token would be canceled, leaving the outer cancellation token unchanged.
Sharp observers may have noticed that the same thing can be done using registrations:
Indeed, logically this is pretty much what is happening: you can think of a linked cancellation token source as a perfectly ordinary cancellation token source along with a registration that cancels it when some other token is canceled.
Multiple Links
The inner cancellation token above is canceled when the outer token is canceled or when its source is explicitly canceled. Similarly, we can pass any number of cancellation tokens into CreateLinkedTokenSource
, and the cancellation token source it returns will be canceled when any of the outer tokens are canceled.
Use Cases
The outer token and the inner cancellation source can really represent anything; linked cancellation tokens are useful whenever you need code to be canceled if “A or B”.
But I suspect the most common use case is when the outer token represents an end-user cancellation request, and the inner token represents a timeout. E.g., this can happen when the business logic includes a timeout-and-retry kind of code pattern, while also allowing the end-user to cancel all the retries with a single button click.
One natural place where this kind of code is used is Polly. Polly will allow you to pass in a cancellation token - an outer token that is under your control. Then it passes a potentially different cancellation token to your execution delegate; this inner token is controlled by Polly. Polly’s pipelines (e.g., timeout) may trigger the inner token to cancel your delegate. Naturally, if your code cancels the outer token passed to Polly, that would flow to the inner token as well. I.e., they are linked.
Taking a simplified code example right from the Polly homepage:
The ExecuteWithRetryAndTimeoutAsync
takes an outer token cancellationToken
and passes it to Polly. Polly then creates a linked inner token (which includes pipeline behaviors such as the timeout), and passes the inner token (token
) to your delegate.
Delegates you pass to Polly should observe the token they get from Polly, not any other tokens!
This is particularly a pitfall when you’re adding Polly pipelines to existing code, e.g., when adding timeouts to this code:
A common mistake is to forget to update the token usage:
In this case, the delegate is still observing cancellationToken
, when it should be observing token
instead:
Sharp Corner: Don’t use OperationCanceledException.CancellationToken
Consider the original example code again:
Now consider some code that may call DoSomethingAsync
and respond to cancellation:
The intent of the handling code is to do something different if the code is canceled due to this particular cancellation source. Unfortunately, this code is problematic in the real world; DoSomethingAsync
may be using a linked cancellation token source, in which case the OperationCanceledException.CancellationToken
would not match cts.Token
, even if that was the source of the cancellation!
This is why I always recommend not using OperationCanceledException.CancellationToken
. A proper solution is to check whether that source has been triggered:
Checking Inner Tokens: Still don’t use OperationCanceledException.CancellationToken
You might be tempted to do this kind of test when using linked cancellation tokens, again to determine what the cancellation source is:
However, this code has the same issue! It’s possible that DoSomethingElseAsync
may itself use a linked cancellation token (or may be changed to use one in the future)!
The solution - again - is to not use OperationCanceledException.CancellationToken
:
Summary
Most of the time you won’t need to use linked cancellation tokens in your code, but linked cancellation tokens are useful when you need them! Some points to remember:
- Dispose your cancellation token sources - including linked cancellation token sources.
- Don’t use
OperationCanceledException.CancellationToken
; useIsCancellationRequested
instead. - For any code that has multiple tokens in scope, be mindful about which one you are observing.