CSharp and Cancelation Token: Lets Get Async

Codelife.ninja C# best practices
Why did the async method break up with the cancellation token?
Because every time things got too intense, it just cancelled! 😆

In C#, the async and await keywords simplify asynchronous programming, but handling cancellations is critical in real-world applications. Users may close applications or cancel operations, and if left unmanaged, these cancellations can lead to unnecessary resource usage or application hang-ups. This article explores how cancellation tokens work within a C# async flow, best practices for implementing them, and examples for common use cases.


What Is a Cancellation Token?

A CancellationToken is a struct in .NET that allows cooperative cancellation between threads. It signals a cancellation request, enabling tasks, methods, or operations to terminate cleanly without leaving tasks hanging. The CancellationTokenSource class generates a CancellationToken, allowing it to be canceled from a central place.

Why Use Cancellation Tokens?

Cancellation tokens serve several purposes in async code:

  1. Resource Efficiency: Allows your application to stop a task that’s no longer needed, freeing up resources.
  2. User Experience: Improves responsiveness by terminating lengthy tasks upon request.
  3. Controlled Execution: Lets developers define specific points where the code checks for cancellation, ensuring a clean termination.

Working with Cancellation Tokens

1. Creating and Passing a Cancellation Token

You can create a cancellation token using CancellationTokenSource. Pass the token to async methods that support cancellation:

var cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;

await PerformAsyncOperation(token);

2. Handling Cancellation in an Async Method

In a cancellable async method, you should periodically check for the token’s status using token.IsCancellationRequested or call token.ThrowIfCancellationRequested() at logical points:

public async Task PerformAsyncOperation(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        // Check if cancellation was requested
        if (cancellationToken.IsCancellationRequested)
        {
            Console.WriteLine("Operation was cancelled.");
            return;
        }

        // Simulate work
        await Task.Delay(100);
    }
}

Or, you can use ThrowIfCancellationRequested() to throw an OperationCanceledException:

public async Task PerformAsyncOperation(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();

        await Task.Delay(100);
    }
}

3. Canceling the Operation

To cancel the operation, call Cancel() on the CancellationTokenSource:

cancellationTokenSource.Cancel();

This signals any method using the associated token to terminate if cancellation checks are implemented.

4. Handling Exceptions and Task Completion

When using ThrowIfCancellationRequested(), a TaskCanceledException or OperationCanceledException is raised. You can handle this in a try-catch block to respond gracefully:

try
{
    await PerformAsyncOperation(token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was canceled successfully.");
}

Advanced Patterns and Scenarios

Timeout-Based Cancellations

You can set a timeout with CancellationTokenSource for scenarios where operations should not exceed a specific duration:

var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await PerformAsyncOperation(cancellationTokenSource.Token);

Linked Cancellation Tokens

To aggregate multiple cancellation requests, you can use CancellationTokenSource.CreateLinkedTokenSource:

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
await PerformAsyncOperation(linkedCts.Token);

Making Long-Running Loops Cancellable

For async flows within loops, checking IsCancellationRequested at each iteration is essential for long-running loops:

public async Task LongRunningLoop(CancellationToken token)
{
    while (true)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100);  // Perform some work
    }
}

Best Practices

  1. Check for Cancellation Periodically: Don’t rely solely on ThrowIfCancellationRequested(); check IsCancellationRequested as a low-cost option for loops.
  2. Avoid Catch-All Exception Blocks: Handle specific exceptions (TaskCanceledException, OperationCanceledException) to respond to cancellations.
  3. Dispose of CancellationTokenSource: It implements IDisposable and should be disposed of when no longer needed.
  4. Use Cancellation Tokens Sparingly: Avoid overusing tokens in complex chains unless each method genuinely benefits from cancellation logic.

Integrating cancellation tokens in C# async code is an efficient way to improve performance, responsiveness, and control. By applying these principles, you can create robust, user-friendly applications that handle cancellations gracefully and avoid unnecessary resource consumption.