Cancellation token multifold

Cancellation token multifold

Combine token sources to get the best out of cancellation model

kndb's photo
kndb
·Nov 27, 2021·

3 min read

This post is a continuation of my previous one Cancellation token and APIs😇

You should check that out for an intro to cancellation tokens and how we can use them in APIs.

In this post we will take a step further and take a look at

  • How to cancel multiple tasks using the same cancellation token
  • Link multiple token sources
    1. Track multiple cancellation tokens simultaneously so that we can cancel a task if either token requests cancellation
    2. Parent token, child token scenarios

Cancel multiple tasks using the same cancellation token

This one is pretty straight forward, we pass on the same token to multiple tasks, if cancellation is requested, all tasks are cancelled.

[HttpPost("~/longoperation")]
public async Task LongRunningOperation(CancellationToken token)
{
    _logger.LogInformation("Calling long running operations");

    Task one = LongRunningOperationOneAsync(token);
    Task two = LongRunningOperationTwoAsync(token);
    await Task.WhenAll(one, two);

    _logger.LogInformation("Done with long running operations");
}

private async Task LongRunningOperationOneAsync(CancellationToken token)
{
    await Task.Delay(1000);
    if (token.IsCancellationRequested)
    {
        _logger.LogError("LongRunningOperationOneAsync was cancelled");
    }
}

private async Task LongRunningOperationTwoAsync(CancellationToken token)
{
    await Task.Delay(1000);
    if (token.IsCancellationRequested)
    {
        _logger.LogError("LongRunningOperationTwoAsync was cancelled");
    }
}

After I initiated the request using Postman, I immediately hit cancel and this is how the application behaves.

image.png

When a cancellation request was encountered, both tasks processed it.

Track multiple cancellation tokens simultaneously

Leverage CreateLinkedTokenSource to combine multiple tokens and build a CancellationTokenSource. This tokensource would start monitoring cancellation in any of the encapsulating tokens.

In our case, we created a new CancellationTokenSource and set it up to auto cancel after 500ms, and then we also have the token that is injected via the API request. We make use of CreateLinkedTokenSource method to generate a new CancellationTokenSource from the 2 tokens, which is then used further in our long running operation.

[HttpPost("~/longoperation")]
public async Task LongRunningOperation(CancellationToken token)
{
    CancellationTokenSource otherTokenSource = new CancellationTokenSource();
    otherTokenSource.CancelAfter(500); 

    // create a new tokensource out of available 2 cancellation tokens
    CancellationTokenSource tokenSourceToUse = CancellationTokenSource.CreateLinkedTokenSource(token, otherTokenSource.Token);

    _logger.LogInformation("Calling long running operation");
    await LongRunningOperationOneAsync(tokenSourceToUse.Token);
    _logger.LogInformation("Done with long running operation");
}

private async Task LongRunningOperationOneAsync(CancellationToken token)
{
    await Task.Delay(1000);
    if (token.IsCancellationRequested)
    {
        _logger.LogError("LongRunningOperationOneAsync was cancelled");
    }
}

After I initiated the request using Postman, I waited for sometime and this is how the application behaves.

image.png

Since our token triggers cancellation after 500ms automatically, our long running operation gets cancelled.

Parent token, child token scenarios

There are times when you would want to cancel child tasks but continue your parent tasks. For cases like these, we again leverage an overload of the CreateLinkedTokenSource method.

[HttpPost("~/longoperation")]
public async Task LongRunningOperation(CancellationToken parentToken)
{
    // create a tokensource out of parentToken
    CancellationTokenSource childTokenSource = CancellationTokenSource.CreateLinkedTokenSource(parentToken);
    childTokenSource.CancelAfter(500);

    _logger.LogInformation("Calling long running operation");
    await LongRunningOperationOneAsync(childTokenSource.Token);
    if (!parentToken.IsCancellationRequested)
    {
        _logger.LogInformation("Parent cancellation was never requested, continue processing 100 other operations");
    }
    _logger.LogInformation("Done with long running operation");
}

private async Task LongRunningOperationOneAsync(CancellationToken token)
{
    await Task.Delay(1000);
    if (token.IsCancellationRequested)
    {
        _logger.LogError("Child cancellation was requested");
    }
}

After I initiated the request using Postman, I waited for sometime and this is how the application behaves.

image.png

Since our child token triggers cancellation after 500ms automatically, our child task gets cancelled, but our parent token still continues to be available.

💡 Note

If parent token is cancelled, all child tokens will be cancelled automatically.

The Task Parallel Library provides us with a wide range of easy to use extensions around cancellation tokens, we can setup cancellation behavior for various mix and matches.

image

I hope this post gave you some insights on how we can leverage CancellationTokens. Thank you for reading. Cheers!