76

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.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
Retrocoder
  • 4,483
  • 11
  • 46
  • 72

4 Answers4

127

You want to use CancellationTokenSource.CreateLinkedTokenSource. It allows you to have a "parent" CancellationToken for a "child" CancellationTokenSource.

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

Arad Alvand
  • 8,607
  • 10
  • 51
  • 71
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 4
    You are quite correct. I started using CancellationTokenSource.CreateLinkedTokenSource but thought it wasn’t working. I forgot that when the token times out it throws an exception. This was being caught further up in my code. This gave the impression it wasn’t working as I had expected. By putting my call in a try catch block it worked fine. – Retrocoder Apr 14 '15 at 09:14
  • 3
    @Retrocoder If you want to only trap the inner token, I would recommend using a pattern like `try { doSomething(ct: childCts.Token); } catch (OperationCancelledException) when (childCts.IsCancellationRequested) {}`. You could put that inside of a retry loop and create the child token source inside the loop. Then, when the parent token cancels, it will bubble all the way up, but when the child token cancels, it just does a retry. I can’t tell from your comment—you might already be doing this correctly ;-). – binki Oct 09 '17 at 19:13
  • 2
    Furthermore, the `OperationCancelledException` has its own `CancellationToken` property which you should check using the `catch (ex) when (condition)` manner. That way you will ensure that you've catched the correct cancellation. – AgentFire May 20 '18 at 12:04
  • 4
    Note that if your code is making many child token sources linked to a single parent token, you should make sure to dispose each child token source to avoid a memory leak. If you don't, the parent will keep all of the child token sources alive, since it has a reference to each of them. In many cases, this is often as simple as writing `using var childCts =`. – Matt Dec 29 '21 at 22:56
  • @Matt Very true. Feel free to add that into the answer itself.. – i3arnon Dec 29 '21 at 23:52
  • This pattern still seemed to cancel my parent token and kill my service – Henry Ing-Simmons Jan 24 '22 at 12:11
  • @binki This wouldn't work because canceling the parent automatically makes all linked token sources canceled as well. The check should probably be something like `when (childCts.IsCancellationRequested && !parentToken.IsCancellationRequested)`. – relatively_random Mar 09 '22 at 08:49
  • 1
    @AgentFire The problem with the exception's `CancellationToken` property is that the consumer of your cancellation token can make their own linked CTS. In that case, even if your CTS caused the cancellation, the exception will say it wasn't you but some unknown token. The best you can do is check your CTS for `IsCancellationRequested` and assume that there are no hidden cancellations independent of your source. (I'd say it's a bug anyway if someone lets an `OperationCanceledException` propagate if the caller didn't ask for it.) – relatively_random Mar 09 '22 at 08:58
  • @relatively_random That could work. Depends on what you’re trying to do. If you have other logic which needs to run still even when the parent token is cancelled, you could instead make sure your loop checks the parent token such as by calling `parentCts.ThrowIfCancellationRequested()` in the loop but outside of the `try{}`. – binki Mar 09 '22 at 13:39
13

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;
John Gietzen
  • 48,783
  • 32
  • 145
  • 190
  • 2
    @Matt, what if we will use `using` for result of `Register(child.Cancel)` ? like that: ```using var _ = cancellationToken.Register(combinedTokenSource.Cancel);``` – valker Dec 29 '21 at 13:44
  • 1
    @valker I like the idea of using a "using" statement to dispose of the CancellationTokenRegistration that is returned by token.Register(child.Cancel). This seems like the best way to prevent the memory leak that would otherwise occur if the parent token is long-lived and many child tokens are created during the parent's lifetime. (I deleted my original comment since it cannot be edited.) – Matt Dec 29 '21 at 22:12
  • 4
    Actually, you don't need to call `Register()` at all. Instead, just use CancellationTokenSource.CreateLinkedTokenSource(token), as demonstrated in the accepted answer by i3arnon – Matt Dec 29 '21 at 22:26
9

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.

binki
  • 7,754
  • 5
  • 64
  • 110
5

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://learn.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.

The Lemon
  • 1,211
  • 15
  • 26
  • The other answers to this question do not require you "to pass around the token source." The `CancellationTokenSource.CreateLinkedTokenSource` method is static, so it can be called anywhere, passing the relevant internal or external token(s). https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.createlinkedtokensource – Matt Dec 29 '21 at 22:35
  • 2
    @Matt it's been so long it took me a while to figure out what last year's lemon was thinking - the other answers use a parent token and a child token source (most of them generate the childTokenSource with the parent token as an argument). The question itself was about linking tokens, and a lot of use cases will require you to link two pre-existing tokens, which is where my answer should help – The Lemon Dec 30 '21 at 23:10