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.
CancellationTokenSource
type that signals cancellation to the token.CancellationTokenSource.Token
property as a token object to the task.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:
return
operator to exit the task execution. In this case, the state of the task will be TaskStatus.RunToCompletion
.OperationCanceledException
type exception via ThrowIfCancellationRequested()
method call. In this case, the state of the task will be TaskStatus.Canceled
.return
Operatorpublic 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'
ThrowIfCancellationRequested()
Method Callpublic 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.
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
.
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.