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:
- Instantiate a
CancellationTokenSource(CTS) in the caller. - Pass its
Token(CancellationToken) into the asynchronous method. - Check the token inside the running operation using either
token.ThrowIfCancellationRequested()or by passing it directly to underlying framework methods that accept tokens (such asHttpClient.SendAsyncorTask.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 callsprogress.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.");
}
}
}