Why Do You Need a Cancellation Token in C# for Tasks?

Written by igorlopushko | Published 2022/03/30
Tech Story Tags: .net | c-sharp | asynchronous | parallel-computing | cancellation-token | cancellation-token-c | learn-to-code | coding | hackernoon-es

TLDRWhen you run a task in C it may take a while to execute it. In some cases, you would like to cancel such a long operation. A CancellationToken(https://://://.ms.com/en-us/dotnet/api//system.threading.cancellating.tasks?view=net-6.0) enables cooperative cancellation between threads, thread pool work items, or Task(http://www.msn.org/s/windows-windows-tasks) The algorithm follows an algorithm to create an object that signals cancellation to the token. Pass the `CancellatedTokenSource.Token` property as a token object to the task.via the TL;DR App

CancellationToken enables cooperative cancellation between threads, thread pool work items, or Task objects. In this article, I would like to discuss the mechanism which is applicable for Task objects.

When you run a task in C#, it may take a while to execute it. In some cases, you would like to cancel such a long operation. There could be a number of reasons: operation timeout, exceeding resource limits, etc.

The Algorithm

  1. Create an object of the CancellationTokenSource type that signals cancellation to the token.
  2. Pass the CancellationTokenSource.Token property as a token object to the task.
  3. Define the behavior of the task for terminating the operation according to the cancellation signal.
  4. Call CancellationTokenSource.Cancel() method which sets CancellationToken.IsCancellationRequested property to a true value. That means that Cancel() method does not cancel the operation itself. It just changes the IsCancellationRequested property value. We as developers have to define cancellation logic by ourselves.

CancellationTokenSource type implements the IDisposable interface and has to be released when a task is completed. It could be done manually by calling Dispose() method or vi using construction.

Sample code to demonstrate the algorithm above:

// initialize cancellation objects
CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
CancellationToken token = cancelTokenSource.Token;

// execute a parallel operation
Task task = new Task(() => { some_operations }, token);
task.Start();

// cancel the operation
cancelTokenSource.Cancel();

// release resources
cancelTokenSource.Dispose();

Let’s discuss step #3 in the detail. There are two ways how to define the logic of task terminating using a cancellation token:

  1. Use return operator to exit the task execution. In this case, the state of the task will be TaskStatus.RunToCompletion.
  2. Throw OperationCanceledException type exception via ThrowIfCancellationRequested() method call. In this case, the state of the task will be TaskStatus.Canceled.

Complete Task via return Operator

public static void Main(string[] args)
{
  CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
  CancellationToken token = cancelTokenSource.Token;

  Task task = new Task(() =>
  {
    for (int i = 1; i < 100; i++)
    {
      if (token.IsCancellationRequested)
      {
        Console.WriteLine("Operation is canceled");
        return;
      }
      Console.WriteLine($"Count is equal to '{i}'");
      //add some timeout to emulate real-life execution
      Thread.Sleep(10);
    }
  }, token);
  task.Start();

  // add some timeout to emulate real-life execution
  Thread.Sleep(100);
  // cancel the parallel operation
  cancelTokenSource.Cancel();
  // wait till the operation is completed
  task.Wait();
  // check the operation status
  Console.WriteLine($"Task Status is equal to '{ task.Status }'");
  // release resources
  cancelTokenSource.Dispose();
}

The result of this execution is following:

Count is equal to '1'
Count is equal to '2'
Count is equal to '3'
Count is equal to '4'
Count is equal to '5'
Operation is canceled
Task Status is equal to 'RanToCompletion'

Complete Task via ThrowIfCancellationRequested() Method Call

public static void Main(string[] args)
{
  CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
  CancellationToken token = cancelTokenSource.Token;

  Task task = new Task(() =>
  {
    for (int i = 1; i < 100; i++)
    {
      if (token.IsCancellationRequested)
        token.ThrowIfCancellationRequested();
      Console.WriteLine($"Count is equal to '{i}'");
      //add some timeout to emulate real-life execution
      Thread.Sleep(10);
    }
  }, token);

  try
  {
    task.Start();
    // add some timeout to emulate real-life execution
    Thread.Sleep(100);
    // cancel the parallel operation
    cancelTokenSource.Cancel();
    // wait till the operation is completed
    task.Wait();
  }
  catch (AggregateException ae)
  {
    foreach (Exception e in ae.InnerExceptions)
    {
      if (e is TaskCanceledException)
        Console.WriteLine("Operation is canceled");
      else
        Console.WriteLine(e.Message);
    }
  }
  finally
  {
    // release resources
    cancelTokenSource.Dispose();
  }

  // check the operation status
  Console.WriteLine($"Task Status is equal to '{ task.Status }'");
}

The result of this execution is following:

Count is equal to '1'
Count is equal to '2'
Count is equal to '3'
Count is equal to '4'
Count is equal to '5'
Operation is canceled
Task Status is equal to 'Canceled'

The thrown exception will appear as an InnerException of the AggregateException. If the task was cancelled via ThrowIfCancellationRequested() method call the exception will be the type of TaskCanceledException. The code checks for this type for proper handling, otherwise, handle another exception reason.

The exception will be thrown only in case when Wait() or WaitAll() method is called for the task. Otherwise, no exception is thrown, just TaskStatus.Canceled is set.

Register Operation Cancellation Handler

Another way to define the logic of the task cancellation is to use Register() method. It registers an Action delegate that will be called when the CancellationToken is cancelled.

public static void Main(string[] args)
{
  CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
  CancellationToken token = cancelTokenSource.Token;

  Task task = new Task(() =>
  {
    int i = 1;
    token.Register(() => 
    { 
      Console.WriteLine("Operation is canceled");
      i = 100;
      Console.WriteLine($"Count is equal to '{i}'");
    });
    for (; i < 100; i++)
    {
      Console.WriteLine($"Count is equal to '{i}'");
      //add some timeout to emulate real-life execution
      Thread.Sleep(10);
    }
  }, token);
  task.Start();

  // add some timeout to emulate real-life execution
  Thread.Sleep(100);
  // cancel the parallel operation
  cancelTokenSource.Cancel();
  // wait till the operation is completed
  task.Wait();
  // check the operation status
  Console.WriteLine($"Task Status is equal to '{ task.Status }'");
  // release resources
  cancelTokenSource.Dispose();
}

The result of this execution is following:

Count is equal to '1'
Count is equal to '2'
Count is equal to '3'
Count is equal to '4'
Count is equal to '5'
Operation is canceled
Count is equal to '100'
Task Status is equal to 'RanToCompletion'

In this code then the cancelTokenSource.Cancel() method is called the delegate defined in the token.Register() method is triggered. In this example, the code sets i variable to 100 value which causes the end of the task execution.

If the code does not wait for the operation competition the task status will be TaskStatus.Running. If Wait() or WaitAll() method is called the task status will be TaskStatus.RanToCompletion.

Summary: Using a Cancellation Token

Cancellation of the task is very important to optimize the logic of your application. You may need to cancel the task for many reasons: operation timeout, exceeding resource limits, etc. You always need to handle the cancellation logic by yourself. You can do it via return operator or via ThrowIfCancellationRequested() method call.


Written by igorlopushko | Programmer, Architect, Teacher
Published by HackerNoon on 2022/03/30