Linking Cancellation Tokens
You want to use CancellationTokenSource.CreateLinkedTokenSource
. It allows to have a "parent" and a "child" CancellationTokenSource
es. Here's a simple example:
var parentCts = new CancellationTokenSource();
var childCts = CancellationTokenSource.CreateLinkedTokenSource(parentCts.Token);
childCts.CancelAfter(1000);
Console.WriteLine("Cancel child CTS");
Thread.Sleep(2000);
Console.WriteLine("Child CTS: {0}", childCts.IsCancellationRequested);
Console.WriteLine("Parent CTS: {0}", parentCts.IsCancellationRequested);
Console.WriteLine();
parentCts.Cancel();
Console.WriteLine("Cancel parent CTS");
Console.WriteLine("Child CTS: {0}", childCts.IsCancellationRequested);
Console.WriteLine("Parent CTS: {0}", parentCts.IsCancellationRequested);
Output as expected:
Cancel child CTS
Child CTS: True
Parent CTS: FalseCancel parent CTS
Child CTS: True
Parent CTS: True
If all you have is a CancellationToken
, instead of a CancellationTokenSource
, then it is still possible to create a linked cancellation token. You would simply use the Register
method to trigger the cancellation of the the (pseudo) child:
var child = new CancellationTokenSource();
token.Register(child.Cancel);
You can do anything you would typically do with a CancellationTokenSource
. For example you can cancel it after a duration and even overwrite your previous token.
child.CancelAfter(cancelTime);
token = child.Token;
As i3arnon already answered, you can do this with CancellationTokenSource.CreateLinkedTokenSource()
. I want to try to show a pattern of how to use such a token when you want to distinguish between cancellation of an overall task versus cancellation of a child task without cancellation of the overall task.
async Task MyAsyncTask(
CancellationToken ct)
{
// Keep retrying until the master process is cancelled.
while (true)
{
// Ensure we cancel ourselves if the parent is cancelled.
ct.ThrowIfCancellationRequested();
using var childCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
// Set a timeout because sometimes stuff gets stuck.
childCts.CancelAfter(TimeSpan.FromSeconds(32));
try
{
await DoSomethingAsync(childCts.Token);
}
// If our attempt timed out, catch so that our retry loop continues.
// Note: because the token is linked, the parent token may have been
// cancelled. We check this at the beginning of the while loop.
catch (OperationCancelledException) when (childCts.IsCancellationRequested)
{
}
}
}
When the Temporary Token expires it must NOT cancel the Master Token.
Note that MyAsyncTask()
’s signature accepts CancellationToken
rather than CancellationTokenSource
. Since the method only has access to the members on CancellationToken
, it cannot accidentally cancel the master/parent token. I recommend that you organize your code in such a way that the CancellationTokenSource
of the master task is visible to as little code as possible. In most cases, this can be done by passing CancellationTokenSource.Token
to methods instead of sharing the reference to the CancellationTokenSource
.
I have not investigated, but there may be a way with something like reflection to forcibly cancel a CancellationToken
without access to its CancellationTokenSource
. Hopefully it is impossible, but if it were possible, it would be considered bad practice and is not something to worry about generally.
Several answers have mentioned creating a linked token source from the parent token. This pattern breaks down if you get passed the child token from elsewhere. Instead you might want to create a linked token source from both your master token and the token being passed to your method.
From Microsoft's documentation: https://docs.microsoft.com/en-us/dotnet/standard/threading/how-to-listen-for-multiple-cancellation-requests
public void DoWork(CancellationToken externalToken)
{
// Create a new token that combines the internal and external tokens.
this.internalToken = internalTokenSource.Token;
this.externalToken = externalToken;
using (CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
{
try {
DoWorkInternal(linkedCts.Token);
}
catch (OperationCanceledException) when (linkedCts.Token.IsCancellationRequested){
if (internalToken.IsCancellationRequested) {
Console.WriteLine("Operation timed out.");
}
else if (externalToken.IsCancellationRequested) {
Console.WriteLine("Cancelling per user request.");
externalToken.ThrowIfCancellationRequested();
}
}
catch (Exception ex)
{
//standard error logging here
}
}
}
Often with passing tokens to methods, the cancellation token is all you have access to. To use the other answer's methods, you might have to re-pipe all the other methods to pass around the token source. This method lets you work with just the token.