CancellationTokenSource
application hang
C# programming
threading issues
debugging techniques

Why CancellationTokenSource hangs an application

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

A CancellationTokenSource can hang an application when a cancellation callback registered via CancellationToken.Register() blocks, when cancellation is triggered on the UI thread causing a deadlock, or when Dispose() is called while callbacks are still executing. The most common pattern is a synchronous callback that tries to access a resource held by the thread calling Cancel(), creating a deadlock. Understanding that Cancel() executes registered callbacks synchronously on the calling thread is the key to avoiding these hangs.

How Cancel() Executes Callbacks

csharp
1var cts = new CancellationTokenSource();
2
3// Register a callback
4cts.Token.Register(() =>
5{
6    Console.WriteLine("Callback executing on: " + Thread.CurrentThread.ManagedThreadId);
7    // This runs SYNCHRONOUSLY on the thread that calls Cancel()
8});
9
10Console.WriteLine("Cancel called on: " + Thread.CurrentThread.ManagedThreadId);
11cts.Cancel();  // Blocks until ALL registered callbacks complete
12Console.WriteLine("Cancel returned");

Cancel() does not return until every registered callback finishes. If any callback blocks, Cancel() blocks, and the calling code hangs.

Deadlock Scenario 1: Callback Waits on the Cancelling Thread

csharp
1private readonly object _lock = new object();
2private CancellationTokenSource _cts = new CancellationTokenSource();
3
4void Setup()
5{
6    _cts.Token.Register(() =>
7    {
8        // DEADLOCK: trying to acquire _lock, but the thread
9        // calling Cancel() already holds _lock
10        lock (_lock)
11        {
12            Console.WriteLine("Cleanup in callback");
13        }
14    });
15}
16
17void StopProcessing()
18{
19    lock (_lock)  // Acquires _lock
20    {
21        _cts.Cancel();  // Executes callback synchronously
22        // Callback tries to acquire _lock → deadlock!
23    }
24}

The callback runs on the same thread that called Cancel(), which already holds the lock. The callback waits for the lock, which will never be released.

Fix: Cancel Outside the Lock

csharp
1void StopProcessing()
2{
3    CancellationTokenSource localCts;
4    lock (_lock)
5    {
6        localCts = _cts;
7        _cts = null;
8    }
9    localCts?.Cancel();  // Cancel outside the lock — no deadlock
10}

Deadlock Scenario 2: UI Thread Synchronization Context

csharp
1// WPF or WinForms application
2private CancellationTokenSource _cts;
3
4async void StartButton_Click(object sender, EventArgs e)
5{
6    _cts = new CancellationTokenSource();
7
8    _cts.Token.Register(() =>
9    {
10        // This callback runs on the UI thread (because Cancel is called on UI thread)
11        // If it tries to update UI synchronously — fine
12        // If it tries to await something — potential issues
13        statusLabel.Text = "Cancelled";
14    });
15
16    try
17    {
18        await DoWorkAsync(_cts.Token);
19    }
20    catch (OperationCanceledException) { }
21}
22
23void CancelButton_Click(object sender, EventArgs e)
24{
25    _cts.Cancel();  // Runs callbacks synchronously on UI thread
26    // If a callback blocks, the UI freezes
27}

Fix: Use CancelAsync() (.NET 8+)

csharp
1// .NET 8+ provides CancelAsync() which does not block
2async void CancelButton_Click(object sender, EventArgs e)
3{
4    await _cts.CancelAsync();  // Callbacks run asynchronously
5}

Fix: Cancel on a Background Thread

csharp
1void CancelButton_Click(object sender, EventArgs e)
2{
3    Task.Run(() => _cts.Cancel());  // Cancel on thread pool — UI stays responsive
4}

Deadlock Scenario 3: Dispose During Active Callbacks

csharp
1var cts = new CancellationTokenSource();
2
3cts.Token.Register(() =>
4{
5    Thread.Sleep(5000);  // Long-running callback
6});
7
8// Cancel and immediately dispose
9cts.Cancel();   // Starts executing callback (blocks for 5s)
10cts.Dispose();  // Called from another thread — may throw or hang

Dispose() waits for callbacks to complete. If called from a different thread while callbacks are running, it blocks until they finish.

Fix: Cancel and Dispose Sequentially

csharp
1try
2{
3    cts.Cancel();
4}
5catch (AggregateException ex)
6{
7    // Handle callback exceptions
8}
9finally
10{
11    cts.Dispose();  // Safe — callbacks are done
12}

Timeout-Based Cancellation Hang

csharp
1// CancellationTokenSource with timeout
2var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
3
4cts.Token.Register(() =>
5{
6    // This callback fires after 5 seconds on a TIMER thread
7    // If it blocks, the timer thread hangs
8    Thread.Sleep(10000);  // Bad — blocks timer thread
9});
10
11// The timeout callback runs on a ThreadPool timer thread
12// Blocking it can starve the thread pool

Fix: Keep Callbacks Non-Blocking

csharp
1cts.Token.Register(() =>
2{
3    // Fire-and-forget async work
4    _ = Task.Run(async () =>
5    {
6        await CleanupResourcesAsync();
7    });
8});

Linked CancellationTokenSource

csharp
1var parentCts = new CancellationTokenSource();
2var childCts = CancellationTokenSource.CreateLinkedTokenSource(parentCts.Token);
3
4childCts.Token.Register(() => Console.WriteLine("Child cancelled"));
5parentCts.Token.Register(() => Console.WriteLine("Parent cancelled"));
6
7parentCts.Cancel();
8// Both callbacks execute — child is cancelled because parent was cancelled
9// If either callback blocks, Cancel() blocks
10
11// IMPORTANT: Always dispose linked CancellationTokenSources
12childCts.Dispose();  // Prevents memory leak from parent registration

Debugging a Hanging Cancel

csharp
1// Add logging to identify which callback hangs
2cts.Token.Register(() =>
3{
4    Console.WriteLine("Callback 1 starting");
5    // ... work ...
6    Console.WriteLine("Callback 1 done");
7});
8
9cts.Token.Register(() =>
10{
11    Console.WriteLine("Callback 2 starting");
12    // If you see "starting" but not "done", this callback is hanging
13});
14
15// Use CancellationTokenSource.TryReset() (.NET 6+) to reuse without recreating
16if (!cts.TryReset())
17{
18    cts.Dispose();
19    cts = new CancellationTokenSource();
20}

Common Pitfalls

  • Blocking in cancellation callbacks: Cancel() executes all registered callbacks synchronously on the calling thread. If a callback acquires a lock, waits on a task, or performs I/O, it blocks Cancel() and can deadlock the application. Keep callbacks lightweight and non-blocking.
  • Calling Cancel() while holding a lock: If any registered callback needs the same lock, you get a deadlock. Always call Cancel() outside of lock blocks by copying the CancellationTokenSource reference first.
  • Not disposing CancellationTokenSource: Undisposed CancellationTokenSource objects leak timer handles (if timeout-based) and linked token registrations. Always dispose, preferably in a finally block.
  • Not disposing linked CancellationTokenSource: CreateLinkedTokenSource registers a callback on the parent token. Without Dispose(), this registration is never removed, causing a memory leak that grows over time.
  • Ignoring AggregateException from Cancel(): If a registered callback throws an exception, Cancel() wraps it in an AggregateException and throws. Unhandled, this crashes the application. Always wrap Cancel() in try-catch if callbacks might throw.

Summary

  • Cancel() executes all registered callbacks synchronously on the calling thread — blocking callbacks hang Cancel()
  • Never call Cancel() while holding a lock that callbacks need — move Cancel() outside the lock
  • Use CancelAsync() (.NET 8+) or Task.Run(() => cts.Cancel()) to avoid blocking the UI thread
  • Keep cancellation callbacks lightweight — offload heavy work to Task.Run
  • Always dispose CancellationTokenSource, especially linked token sources, to prevent memory leaks
  • Wrap Cancel() in try-catch to handle exceptions from callbacks

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions