C#–How to gracefully cancel the execution of the process Cancellation

Overview

Starting with .NET Framework 4, .NET uses a unified model for cooperative cancellation of asynchronous or long-running synchronous threads. The model is based on a lightweight object called a CancellationToken. This object is done by passing a token to each thread (through the chain are passed sequentially). A single thread can sequentially pass copies of tokens to other threads.

After that, at an appropriate time, the object that created the token can use the token to request the thread to stop. Only the request object can issue a cancellation request, and each listener is responsible for listening to the request and responding to the cancellation request in an appropriate and timely manner.

The general pattern for implementing a cooperative cancellation model is:

1. Instantiate a CancellationTokenSource object that manages cancellation and sends cancellation notifications to individual cancellation tokens.

2. The Token property of the CancellationTokenSource object can return a Token object, and we can send the Token object to each process or Task that monitors the cancellation.

3. Provide a mechanism for responding to cancellation for each task or thread.

4. Call the CancellationTokenSource.Cancel() method to cancel the thread or task.

[tips] After we use the cancellation token to cancel the thread, we should make sure to call the CancellationTokenSource.Dispose() method to release any unmanaged resources it holds. .

The figure below shows how the Token attribute object in the CancellationTokenSource object is passed to other threads.

The cooperative cancellation model makes it easier to create cancellation-aware applications and libraries, which support the following features:

1. Cancellation is formal and will not be imposed on listeners. The listener determines how to terminate gracefully in response to a cancel request.

2. Requests are different from monitoring. The object of the thread calling cancelable, which controls when (if ever) cancellation is requested.

3. The requested object can send a cancellation request to all token copies by using only one method.

4. The listener can monitor multiple Tokens at the same time by connecting multiple Tokens into a linked Token.

5. The user code can notice and respond to the cancel request of the library code, and the library code can notice and respond to the cancel request of the user code.

6. The listener can be notified to execute the cancellation request through polling, callback registration or waiting for the wait handle.

Types related to canceling threads

The cancellation framework is implemented as a set of related types, listed in the following table.

CancellationTokenSource This object creates a cancellation token and distributes cancellation requests to all copies of the cancellation token.
CancellationToken A lightweight value type passed to one or more listeners, usually as method parameters. The listener monitors the value of the token’s IsCancellationRequested property via a poll, callback, or wait handle.
OperationCanceledException This is an overload of the exception constructor that accepts a CancellationToken as a parameter. Listeners can optionally throw this exception to verify the source of the cancellation and to notify other listeners that responded to the cancellation request.

Cancellation models are integrated into .net in several types. The most important are System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task, and System.Linq.ParallelEnumerable. It is recommended to use all new library and application code to implement cooperative city cancellation mode.

Code Example

In the following example, the request object creates a CancellationTokenSource object and then passes that object’s Token property to the cancelable process. The thread receiving the request monitors the value of the Token’s IsCancellationRequested property by polling. When the value becomes true, the listener can be terminated in any suitable way. In this case, the method simply exits, which is all that is needed in many cases.

using System;
using System. Threading;

public class Example
{
    public static void Main()
    {
        //Create the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        // Pass the token to the cancelable operation.
        ThreadPool. QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        Thread. Sleep(2500);

        // Request cancellation.
        cts.Cancel();
        Console.WriteLine("Cancellation set in token source...");
        Thread. Sleep(2500);
        // Cancellation should have happened, so call Dispose.
        cts. Dispose();
    }

    // Thread 2: The listener
    static void DoSomeWork(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken token = (CancellationToken) obj;

        for (int i = 0; i < 100000; i ++ )
        {
            if (token. IsCancellationRequested)
            {
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1);
                // Perform cleanup if necessary.
                //...
                // Terminate the operation.
                break;
            }
            // Simulate some work.
            Thread.SpinWait(500000);
        }
    }
}
// The example displays output like the following:
// Cancellation set in token source...
// In iteration 1430, cancellation has been requested...

Operation Cancellation vs Object Cancellation

In the cooperative cancellation framework, cancellation refers to operations (operations performed in threads), not objects. A cancel request means that the operation should stop as soon as possible after performing any required cleanup. A cancellation token should point to a “cancellable operation”, no matter how that operation is implemented in your program. After a token’s IsCancellationRequested property is set to true, it cannot be reset to false. Therefore, cancellation tokens cannot be reused after being canceled.

If you need an object cancellation mechanism, you can cancel the mechanism based on the operation by calling CancellationToken. Register method, as shown in the example below.

using System;
using System. Threading;

class CancelableObject
{
    public string id;

    public CancelableObject(string id)
    {
        this.id = id;
    }

    public void Cancel()
    {
        Console.WriteLine("Object {0} Cancel callback", id);
        // Perform object cancellation here.
    }
}

public class Example1
{
    public static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        // User defined Class with its own method for cancellation
        var obj1 = new CancelableObject("1");
        var obj2 = new CancelableObject("2");
        var obj3 = new CancelableObject("3");

        // Register the object's cancel method with the token's
        // cancellation request.
        token.Register(() => obj1.Cancel());
        token.Register(() => obj2.Cancel());
        token.Register(() => obj3.Cancel());

        // Request cancellation on the token.
        cts.Cancel();
        // Call Dispose when we're done with the CancellationTokenSource.
        cts. Dispose();
    }
}
// The example displays the following output:
// Object 3 Cancel callback
// Object 2 Cancel callback
// Object 1 Cancel callback

If an object supports multiple concurrent cancelable operations, a different token can be passed in for each different cancelable operation. This way, one operation can be canceled without affecting other operations.

Listen and respond to cancel requests

In a user delegate, the implementor of a cancelable operation decides how to terminate the operation in response to a cancellation request. In many cases, a user delegate can just perform any required cleanup and return immediately.

However, in more complex cases, it may be necessary for the user delegate to notify library code that cancellation has occurred. In this case, the correct way to abort the operation is for the delegate to call the ThrowIfCancellationRequested method, which will cause an OperationCanceledException to be thrown. Library code can catch this exception on the user delegate thread and examine the exception’s token to determine whether the exception represents a cancellation of a collaboration or some other exceptional condition. In this case, the correct way to terminate the operation is for the delegate to call the ThrowIfCancellationRequested method, which will cause an OperationCanceledException to be thrown. Library code can catch this exception on the user delegate thread and examine the exception’s token to determine whether the exception represents a cancellation of a collaboration or some other exceptional condition.

Polling listener

For cyclic or recursive long-running computations, you can listen for cancellation requests by periodically polling the value of CancellationToken.IsCancellationRequested. If it evaluates to true, the method should clean up and terminate as soon as possible. The optimal frequency of polling depends on the type of application. Developers can determine the optimal polling frequency for any given program. Polling itself does not significantly affect performance. The program example below shows a possible polling method.

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int col = 0; col < rect.columns & amp; & amp; !token.IsCancellationRequested; col ++ ) {
      // Assume that we know that the inner loop is very fast.
      // Therefore, polling once per column in the outer loop condition
      // is sufficient.
      for (int row = 0; row < rect.rows; row ++ ) {
         // Simulating work.
         Thread.SpinWait(5_000);
         Console.Write("{0},{1}", col, row);
      }
   }

   if (token. IsCancellationRequested) {
      // Cleanup or undo here if necessary...
      Console.WriteLine("\r\\
Operation canceled");
      Console.WriteLine("Press any key to exit.");

      // If using Task:
      // token. ThrowIfCancellationRequested();
   }
}

The program code below is a detailed implementation:

using System;
using System. Threading;

public class ServerClass
{
   public static void StaticMethod(object obj)
   {
      CancellationToken ct = (CancellationToken) obj;
      Console.WriteLine("ServerClass.StaticMethod is running on another thread.");

      // Simulate work that can be canceled.
      while (!ct. IsCancellationRequested) {
         Thread.SpinWait(50000);
      }
      Console.WriteLine("The worker thread has been canceled. Press any key to exit.");
      Console. ReadKey(true);
   }
}

public class Simple
{
   public static void Main()
   {
      // The Simple class controls access to the token source.
      CancellationTokenSource cts = new CancellationTokenSource();

      Console.WriteLine("Press 'C' to terminate the application...\\
");
      // Allow the UI thread to capture the token source, so that it
      // can issue the cancel command.
      Thread t1 = new Thread(() => { if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
                                     cts.Cancel(); } );

      // ServerClass sees only the token, not the token source.
      Thread t2 = new Thread(new ParameterizedThreadStart(ServerClass.StaticMethod));
      // Start the UI thread.

      t1. Start();

      // Start the worker thread and pass it the token.
      t2.Start(cts.Token);

      t2. Join();
      cts. Dispose();
   }
}
// The example displays the following output:
// Press 'C' to terminate the application...
//
// ServerClass.StaticMethod is running on another thread.
// The worker thread has been canceled. Press any key to exit.

Monitoring through callback registration

Certain operations performed in this manner may block, preventing the value of the cancellation token from being checked in time. For these cases, you can register a callback method that unblocks the method when a cancellation request is received.
The Register method returns a CancellationTokenRegistration object specifically for this purpose. The following example shows how to use the Register method to cancel an asynchronous web request.

using System;
using System.Net;
using System. Threading;

class Example4
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        StartWebRequest(cts.Token);

        // cancellation will cause the web
        // request to be canceled
        cts.Cancel();
    }

    static void StartWebRequest(CancellationToken token)
    {
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted + = (s, e) => Console.WriteLine("Request completed.");

        // Cancellation on the token will
        // call CancelAsync on the WebClient.
        token. Register(() =>
        {
            wc. CancelAsync();
            Console.WriteLine("Request canceled!");
        });

        Console.WriteLine("Starting request.");
        wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
    }
}

The CancellationTokenRegistration object manages thread synchronization and ensures that the callback will stop executing at a precise point in time.

To ensure system responsiveness and avoid deadlocks, the following guidelines must be followed when registering callbacks:

1. The callback method should be fast because it is called synchronously, so the call to Cancel will not return until the callback returns.

2. If Dispose is called while the callback is running, and the lock that the callback is waiting for is held, the program may deadlock. After Dispose returns, you can release any resources needed by the callback.

3. Callbacks should not execute any manual threads or use SynchronizationContext in callbacks. If the callback must run on a specific thread, use the System.Threading.CancellationTokenRegistration constructor, which enables you to specify that the target syncContext is the active SynchronizationContext.Current. Executing a manual thread in a callback can cause a deadlock.

Use WaitHandle to listen

A cancelable operation may block while waiting for a synchronization primitive such as System.Threading.manualresetevent or System.Threading.Semaphore. You can use the CancellationToken.WaitHandle property to make an operation wait for both an event and a cancellation request. The CancellationToken’s wait handle will be signaled in response to a cancellation request, and the method can use the return value of the WaitAny() method to determine whether it was the cancellation token that signaled. The operation can then exit directly, or throw an OperationCanceledException.

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle. WaitAny(new WaitHandle[] { mre, token. WaitHandle },
                          new TimeSpan(0, 0, 20));

Both System.Threading.ManualResetEventSlim and System.Threading.SemaphoreSlim support canceling frames in their Wait() methods. You can pass a CancellationToken to the method, and when the request is cancelled, the event will be woken up and an OperationCanceledException will be thrown.

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);

The following example uses ManualResetEvent to demonstrate how to unblock a wait handle that does not support uniform cancellation.

using System;
using System. Threading;
using System. Threading. Tasks;

class CancelOldStyleEvents
{
    // Old-style MRE that doesn't support unified cancellation.
    static ManualResetEvent mre = new ManualResetEvent(false);

    static void Main()
    {
        var cts = new CancellationTokenSource();

        // Pass the same token source to the delegate and to the task instance.
        Task.Run(() => DoWork(cts.Token), cts.Token);
        Console.WriteLine("Press s to start/restart, p to pause, or c to cancel.");
        Console.WriteLine("Or any other key to exit.");

        // Old-style UI thread.
        bool goAgain = true;
        while (goAgain)
        {
            char ch = Console.ReadKey(true).KeyChar;

            switch (ch)
            {
                case 'c':
                    cts.Cancel();
                    break;
                case 'p':
                    mre. Reset();
                    break;
                case 's':
                    mre. Set();
                    break;
                default:
                    goAgain = false;
                    break;
            }

            Thread. Sleep(100);
        }
        cts. Dispose();
    }

    static void DoWork(CancellationToken token)
    {
        while (true)
        {
            // Wait on the event if it is not signaled.
            int eventThatSignaledIndex =
                   WaitHandle. WaitAny(new WaitHandle[] { mre, token. WaitHandle },
                                      new TimeSpan(0, 0, 20));

            // Were we canceled while waiting?
            if (eventThatSignaledIndex == 1)
            {
                Console.WriteLine("The wait operation was canceled.");
                throw new OperationCanceledException(token);
            }
            // Were we canceled while running?
            else if (token. IsCancellationRequested)
            {
                Console.WriteLine("I was canceled while running.");
                token. ThrowIfCancellationRequested();
            }
            // Did we time out?
            else if (eventThatSignaledIndex == WaitHandle. WaitTimeout)
            {
                Console.WriteLine("I timed out.");
                break;
            }
            else
            {
                Console. Write("Working... ");
                // Simulating work.
                Thread.SpinWait(5000000);
            }
        }
    }
}

The following example uses ManualResetEventSlim to demonstrate how to unblock a coordination primitive that supports uniform cancellation. The same approach can be used for other lightweight coordination primitives such as SemaphoreSlim and CountdownEvent.

using System;
using System. Threading;
using System. Threading. Tasks;

class CancelNewStyleEvents
{
   // New-style MRESlim that supports unified cancellation
   // in its Wait methods.
   static ManualResetEventSlim mres = new ManualResetEventSlim(false);

   static void Main()
   {
      var cts = new CancellationTokenSource();

      // Pass the same token source to the delegate and to the task instance.
      Task.Run(() => DoWork(cts.Token), cts.Token);
      Console.WriteLine("Press c to cancel, p to pause, or s to start/restart,");
      Console.WriteLine("or any other key to exit.");

      // New-style UI thread.
         bool goAgain = true;
         while (goAgain)
         {
             char ch = Console.ReadKey(true).KeyChar;

             switch (ch)
             {
                 case 'c':
                     // Token can only be canceled once.
                     cts.Cancel();
                     break;
                 case 'p':
                     mres. Reset();
                     break;
                 case 's':
                     mres. Set();
                     break;
                 default:
                     goAgain = false;
                     break;
             }

             Thread. Sleep(100);
         }
         cts. Dispose();
     }

     static void DoWork(CancellationToken token)
     {

         while (true)
         {
             if (token. IsCancellationRequested)
             {
                 Console.WriteLine("Canceled while running.");
                 token. ThrowIfCancellationRequested();
             }

             // Wait on the event to be signaled
             // or the token to be canceled,
             // whichever comes first. The token
             // will throw an exception if it is canceled
             // while the thread is waiting on the event.
             try
             {
                 // mres is a ManualResetEventSlim
                 mres.Wait(token);
             }
             catch (OperationCanceledException)
             {
                 // Throw immediately to be responsive. The
                 // alternative is to do one more item of work,
                 // and throw on next iteration, because
                 // IsCancellationRequested will be true.
                 Console.WriteLine("The wait operation was canceled.");
                 throw;
             }

             Console.Write("Working...");
             // Simulating work.
             Thread.SpinWait(500000);
         }
     }
 }

Listen to multiple tokens at the same time

In some cases, a listener must listen for multiple cancellation tokens at the same time. For example, a cancelable operation may have to monitor the inner cancellation token in addition to the outer token passed in as a method parameter. To do this, create a linked token source, which can concatenate two or more tokens into one token, as shown in the example below.

using System;
using System. Threading;
using System. Threading. Tasks;

class LinkedTokenSourceDemo
{
    static void Main()
    {
        WorkerWithTimer worker = new WorkerWithTimer();
        CancellationTokenSource cts = new CancellationTokenSource();

        // Task for UI thread, so we can call Task. Wait wait on the main thread.
        Task.Run(() =>
        {
            Console.WriteLine("Press 'c' to cancel within 3 seconds after work begins.");
            Console.WriteLine("Or let the task time out by doing nothing.");
            if (Console. ReadKey(true). KeyChar == 'c')
                cts.Cancel();
        });

        // Let the user read the UI message.
        Thread. Sleep(1000);

        // Start the worker task.
        Task task = Task.Run(() => worker.DoWork(cts.Token), cts.Token);

        try
        {
            task.Wait(cts.Token);
        }
        catch (OperationCanceledException e)
        {
            if (e.CancellationToken == cts.Token)
                Console.WriteLine("Canceled from UI thread throwing OCE.");
        }
        catch (AggregateException ae)
        {
            Console.WriteLine("AggregateException caught: " + ae.InnerException);
            foreach (var inner in ae. InnerExceptions)
            {
                Console.WriteLine(inner.Message + inner.Source);
            }
        }

        Console.WriteLine("Press any key to exit.");
        Console. ReadKey();
        cts. Dispose();
    }
}

class WorkerWithTimer
{
    CancellationTokenSource internalTokenSource = new CancellationTokenSource();
    CancellationToken internalToken;
    CancellationToken externalToken;
    Timer timer;

    public WorkerWithTimer()
    {
        // A toy cancellation trigger that times out after 3 seconds
        // if the user does not press 'c'.
        timer = new Timer(new TimerCallback(CancelAfterTimeout), null, 3000, 3000);
    }

    public void DoWork(CancellationToken externalToken)
    {
        // Create a new token that combines the internal and external tokens.
        this.internalToken = internalTokenSource.Token;
        this.externalToken = externalToken;

        using (CancellationTokenSource linkedCts =
                CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
        {
            try
            {
                DoWorkInternal(linkedCts. Token);
            }
            catch (OperationCanceledException)
            {
                if (internalToken. IsCancellationRequested)
                {
                    Console.WriteLine("Operation timed out.");
                }
                else if (externalToken. IsCancellationRequested)
                {
                    Console.WriteLine("Cancelling per user request.");
                    externalToken. ThrowIfCancellationRequested();
                }
            }
        }
    }

    private void DoWorkInternal(CancellationToken token)
    {
        for (int i = 0; i < 1000; i ++ )
        {
            if (token. IsCancellationRequested)
            {
                // We need to dispose the timer if cancellation
                // was requested by the external token.
                timer. Dispose();

                // Throw the exception.
                token. ThrowIfCancellationRequested();
            }

            // Simulating work.
            Thread.SpinWait(7500000);
            Console. Write("working... ");
        }
    }

    public void CancelAfterTimeout(object? state)
    {
        Console.WriteLine("\r\\
Timer fired.");
        internalTokenSource. Cancel();
        timer. Dispose();
    }
}

Note that you must call Dispose on the linked token source when you are done disposing of it.

When a linked token throws an operation cancel, the token passed to the exception is the linked token, not the predecessor token. To determine which of the tokens is canceled, check the status of the predecessor token directly.

In this case, AggregateException should not be thrown, but it will be caught here because in real scenario, any exception other than OperationCanceledException thrown from task delegate is wrapped in AggregateException.