Handling Cancellation, Timeouts, and Progress in C

Managing long-running asynchronous operations in C# requires responsive execution control and clear feedback mechanisms. This article covers how to implement cooperative cancellation using CancellationTokenSource, enforce strict execution deadlines with timeouts, and stream real-time status updates back to calling threads using the IProgress<T> interface.

Cooperative Cancellation with CancellationToken

Cancellation in .NET is cooperative, meaning the running task must actively check if a cancellation has been requested and terminate gracefully.

To implement cancellation:

  1. Instantiate a CancellationTokenSource (CTS) in the caller.
  2. Pass its Token (CancellationToken) into the asynchronous method.
  3. Check the token inside the running operation using either token.ThrowIfCancellationRequested() or by passing it directly to underlying framework methods that accept tokens (such as HttpClient.SendAsync or Task.Delay).
public async Task RunTaskAsync(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        // Throws OperationCanceledException if cancellation was requested
        cancellationToken.ThrowIfCancellationRequested();

        await Task.Delay(100, cancellationToken);
    }
}

To trigger the cancellation, invoke cts.Cancel() from the calling context (e.g., a button click or shutdown event).

Implementing Timeouts

Timeouts can be enforced automatically without manual timers by leveraging built-in features of CancellationTokenSource.

Approach 1: Timed CancellationTokenSource

Initialize the CTS with a TimeSpan or call CancelAfter:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
    await RunTaskAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("The operation timed out.");
}

Approach 2: Combining User Cancellation and Timeouts

To support both a manual cancellation request (e.g., from a user) and a strict execution timeout, link multiple tokens together using CancellationTokenSource.CreateLinkedTokenSource:

using var userCts = new CancellationTokenSource();
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(userCts.Token, timeoutCts.Token);

try
{
    await RunTaskAsync(linkedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
    Console.WriteLine("Task failed due to timeout.");
}
catch (OperationCanceledException) when (userCts.IsCancellationRequested)
{
    Console.WriteLine("Task was canceled by the user.");
}

Reporting Progress with IProgress<T>

Directly updating UI elements or caller state from inside a background task can cause thread-safety and cross-threading issues. The standard .NET pattern uses the IProgress<T> interface and the Progress<T> class.

  • Progress<T> captures the current synchronization context (such as the UI thread) upon creation and invokes its callback on that thread.
  • The worker method accepts an IProgress<T> parameter and calls progress.Report(value).
public async Task ProcessDataAsync(IProgress<int> progress, CancellationToken cancellationToken)
{
    for (int i = 1; i <= 100; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();

        await Task.Delay(50, cancellationToken);

        // Report progress percentage safely
        progress?.Report(i);
    }
}

Putting It All Together

The following complete example combines progress reporting, manual cancellation, and an enforced timeout into a single unified pattern:

public async Task ExecuteLongRunningOperationAsync()
{
    using var userCts = new CancellationTokenSource();
    using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(userCts.Token, timeoutCts.Token);

    // Progress updates are dispatched automatically to the calling thread
    var progress = new Progress<int>(percent =>
    {
        Console.WriteLine($"Progress: {percent}%");
    });

    try
    {
        await ProcessDataAsync(progress, linkedCts.Token);
        Console.WriteLine("Operation completed successfully.");
    }
    catch (OperationCanceledException)
    {
        if (timeoutCts.IsCancellationRequested)
        {
            Console.WriteLine("Execution aborted: Operation timed out.");
        }
        else
        {
            Console.WriteLine("Execution aborted: User requested cancellation.");
        }
    }
}