Linking Cancellation Tokens

C#.NetCancellationtokensourceCancellation Token

C# Problem Overview


I use a cancellation token that is passed around so that my service can be shut down cleanly. The service has logic that keeps trying to connect to other services, so the token is a good way to break out of these retry loops running in separate threads. My problem is that I need to make a call to a service which has internal retry logic but to return after a set period if a retry fails. I would like to create a new cancellation token with a timeout which will do this for me. The problem with this is that my new token isn't linked to the “master” token so when the master token is cancelled, my new token will still be alive until it times-out or the connection is made and it returns. What I would like to do is link the two tokens together so that when the master one is cancelled my new one will also cancel. I tried using the CancellationTokenSource.CreateLinkedTokenSource method but when my new token timed-out, it also cancelled the master token. Is there a way to do what I need to do with tokens or will it require changes to the retry logic (probably not going to be able to do this easily)

Here is what I want to do:

Master Token – passed around various functions so that the service can shut down cleanly. Temporary Token – passed to a single function and set to timeout after one minute

If the Master Token is cancelled, the Temporary Token must also be cancelled.

When the Temporary Token expires it must NOT cancel the Master Token.

C# Solutions


Solution 1 - C#

You want to use CancellationTokenSource.CreateLinkedTokenSource. It allows to have a "parent" and a "child" CancellationTokenSourcees. 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: False

> Cancel parent CTS
Child CTS: True
Parent CTS: True

Solution 2 - C#

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;

Solution 3 - C#

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.

Solution 4 - C#

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRetrocoderView Question on Stackoverflow
Solution 1 - C#i3arnonView Answer on Stackoverflow
Solution 2 - C#John GietzenView Answer on Stackoverflow
Solution 3 - C#binkiView Answer on Stackoverflow
Solution 4 - C#The LemonView Answer on Stackoverflow