Mastering Task Cancellation in C#

Graceful Cancellation Patterns for Responsive and Reliable Applications

Posted by Hüseyin Sekmenoğlu on December 18, 2018 Backend Development Performance & Optimization System Design

📌 Introduction

Cancellation is a critical concept in modern task-based programming. Whether you're building responsive user interfaces or managing long-running background operations, enabling cancellation allows your application to be more robust, resource-conscious and user-friendly.

In this article, we’ll explore how to implement cooperative cancellation in C# using the CancellationToken and CancellationTokenSource classes, demonstrate real-world examples and highlight best practices to avoid leaving your application in an inconsistent state.


🚦 Why Cancellation Matters

Task cancellation is useful in several common scenarios:

  • Gracefully stopping a task that is no longer needed

  • Releasing critical or limited resources

  • Enhancing application responsiveness and user experience

Rather than forcibly terminating a task, C# promotes a cooperative cancellation model. This ensures both the task and the calling code can coordinate the cancellation logic, maintaining application integrity.


🧰 The Core Cancellation Components in C#

Two key components drive task cancellation:

  • CancellationTokenSource: Signals a cancellation request and produces a CancellationToken.

  • CancellationToken: Passed to the task so it can check for cancellation requests.

Example:

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

To link this token with a task:

var printTask = Task.Run(() => {
    // Your task logic here
}, token);

👤 User-Initiated Cancellation

🧪 Example 1: Cooperative Exit with Return

In this example, we create a task that prints numbers from 0 to 99 and can be canceled by user input:

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

var printTask = Task.Run(() => {
    for (int i = 0; i < 100; i++) {
        if (token.IsCancellationRequested) {
            Console.WriteLine("Cancelling the print activity.");
            return;
        }
        Console.WriteLine($"{i}");
        Thread.Sleep(500);
    }
}, token);

Console.WriteLine("Enter c to cancel the task.");
if (Console.ReadKey().KeyChar == 'c') {
    Console.WriteLine("\nRaising the cancellation request.");
    tokenSource.Cancel();
}

printTask.Wait();
Console.WriteLine($"The final status of printTask is: {printTask.Status}");

✅ Output

0
1
2
c
Raising the cancellation request.
Cancelling the print activity.
The final status of printTask is: RanToCompletion

Even though the task was canceled, the final status is RanToCompletion because the task ended via return, not via exception.


⚠️ Alternative: Exception-Based Cancellation

🧪 Example 2: Using OperationCanceledException

To explicitly mark a task as canceled:

if (token.IsCancellationRequested) {
    Console.WriteLine("Cancelling the print activity.");
    throw new OperationCanceledException(token);
}

✅ Output

Cancelling the print activity.
Caught: TaskCanceledException, Message: A task was canceled.
The final status of printTask is: Canceled

This approach is preferred if you want the task status to be Canceled.


⏱️ Timeout Cancellation

Instead of waiting indefinitely, you can schedule cancellation:

tokenSource.CancelAfter(2000);

This triggers a cancellation request after 2000 milliseconds, unless the user cancels earlier. You can combine both approaches.


🛰️ Monitoring Cancellation Events

C# offers several mechanisms to monitor when a cancellation occurs:

🔔 Using Register

token.Register(() => {
    Console.WriteLine("Cancelling the print activity. [Using event subscription]");
});

🧱 Using WaitHandle.WaitOne

Task.Run(() => {
    token.WaitHandle.WaitOne();
    Console.WriteLine("Cancelling the print activity. [Using WaitHandle]");
});

Both methods allow you to observe and react when the cancellation is requested.


🧪 Demonstration: Monitoring in Action

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

token.Register(() => {
    Console.WriteLine("Cancelling the print activity. [Using event subscription]");
});

var printTask = Task.Run(() => {
    for (int i = 0; i < 100; i++) {
        token.ThrowIfCancellationRequested();
        Console.WriteLine($"{i}");
        Thread.Sleep(500);
    }
}, token);

Task.Run(() => {
    token.WaitHandle.WaitOne();
    Console.WriteLine("Cancelling the print activity. [Using WaitHandle]");
});

Console.WriteLine("Enter c to cancel the task.");
if (Console.ReadKey().KeyChar == 'c') {
    Console.WriteLine("\nTask cancellation requested.");
    tokenSource.Cancel();
}

while (!printTask.IsCompleted) { }
Console.WriteLine($"The final status of printTask is: {printTask.Status}");

🧩 Multiple Cancellation Tokens

Sometimes, multiple conditions may trigger cancellation. In that case, use a linked token.

var normalCancellation = new CancellationTokenSource();
var unexpectedCancellation = new CancellationTokenSource();

var compositeToken = CancellationTokenSource.CreateLinkedTokenSource(
    normalCancellation.Token,
    unexpectedCancellation.Token
);

var printTask = Task.Run(() => {
    for (int i = 0; i < 100; i++) {
        compositeToken.Token.ThrowIfCancellationRequested();
        Console.WriteLine($"{i}");
        Thread.Sleep(500);
    }
}, compositeToken.Token);

You can cancel from any source:

normalCancellation.Cancel(); // User input  
unexpectedCancellation.Cancel(); // Emergency trigger

🧾 Summary

Task cancellation in C# enables cooperative control over long-running operations, allowing developers to:

  • Gracefully stop tasks via user interaction

  • Automate cancellation using timeouts

  • Monitor and respond to cancellation via event registration

  • Combine multiple cancellation sources for complex scenarios

By designing your applications with proper cancellation handling, you ensure responsiveness, reliability and clean resource management. All essential traits of a modern, professional .NET application.