A 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
- Create an object of the
CancellationTokenSource
type that signals cancellation to the token. - Pass the
CancellationTokenSource.Token
property as a token object to the task. - Define the behavior of the task for terminating the operation according to the cancellation signal.
- Call
CancellationTokenSource.Cancel()
method which setsCancellationToken.IsCancellationRequested
property to atrue
value. That means thatCancel()
method does not cancel the operation itself. It just changes theIsCancellationRequested
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:
- Use
return
operator to exit the task execution. In this case, the state of the task will beTaskStatus.RunToCompletion
. - Throw
OperationCanceledException
type exception viaThrowIfCancellationRequested()
method call. In this case, the state of the task will beTaskStatus.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.