Cancel a task with OperationCanceledException

Why the OperationCanceledException should be used over a soft cancellation

Home DailyDrop

Daily Knowledge Drop

When cancelling tasks using a CancellationTokenSource, rather than do a soft cancellation, an OperationCanceledException should be thrown.


Cancellation Token

A quick summary of cancellation tokens - they "enable cooperative cancellation between threads, thread pool work items or Task objects".

Basically cancellation tokens are instantiated outside a particular thread, and then passed into the thread, to allow for cancellation from outside the thread.


Examples

In each of the below examples, there is a "long" running process which runs for 10 seconds. A cancellation token is passed in and checked every 1 second to see if a cancellation has been requested - if requested, then an action will be perform to abort/cancel the long running process.

Soft cancellation

The first example will show how to do a "soft cancellation".

Here, if a cancellation is requested, the developer will decide how to manually cause the method to end its execution.

var cts = new CancellationTokenSource();
var token = cts.Token;

// start the long running process
var longProcessTask = Task.Run(async () =>
{
    for (var i = 0; i < 10; i++)
    {
        // If the token has been requested to cancel, break 
        / out of the loop causing the long processing and 
        // effectively cause the long running task to finish
        if (token.IsCancellationRequested)
            break; // exit from the loop

        Console.WriteLine("Processing...");
        await Task.Delay(1000);
    }
}, token);

// If the user presses a key, the token is cancelled
Console.ReadKey();
cts.Cancel();

await longProcessTask;

Console.WriteLine($"Task is completed: {longProcessTask.IsCompleted}");

The problem with this approach, is that there is no indication if the task actually finished to completion or not.

If the longProcessTask was required to finish, before another process was to kick off - we do not know if the longProcessTask finished due to executing to its end, or if it finished due to be cancelled early.


OperationCanceledException

Rather than a "soft cancellation, a OperationCanceledException should be used. This provides a clear way of determining if the task was cancelled or finished successfully.

var cts = new CancellationTokenSource();
var token = cts.Token;

// start the long running process
var longProcessTask = Task.Run(async () =>
{
    for (var i = 0; i < 10; i++)
    {
        // If the token has been requested to cancel, 
        // throw an _OperationCanceledException_
        if (token.IsCancellationRequested)
            throw new OperationCanceledException();

        Console.WriteLine("Processing...");
        await Task.Delay(1000);
    }
}, token);

// The longProcessTask is awaited inside a try-catch block. 
try
{
    Console.ReadKey();
    cts.Cancel();

    await longProcessTask;

    Console.WriteLine($"Task is completed: {longProcessTask.IsCompleted}");
}
catch (OperationCanceledException e)
{
    Console.WriteLine($"{nameof(OperationCanceledException)} " +
        $"thrown with message: {e.Message}");
}

With this method, when task is cancelled we can now tell vs the task actually finishing to completion.


ThrowIfCancellationRequested

There is a slight improvement which can be made on the above, which is the recommended way of dealing with a cancelled task, and that is to use the ThrowIfCancellationRequested method on a token.

Looking at the source code for the method one can see that its basically doing the same as the above, just wrapped up neatly into a method.

var cts = new CancellationTokenSource();
var token = cts.Token;

// start the long running process
var longProcessTask = Task.Run(async () =>
{
    for (var i = 0; i < 10; i++)
    {
        // check if a cancellation has been requested
        token.ThrowIfCancellationRequested();

        Console.WriteLine("Processing...");
        await Task.Delay(1000);
    }
}, token);

try
{
    Console.ReadKey();
    cts.Cancel();

    await longProcessTask;

    Console.WriteLine($"Task is completed: {longProcessTask.IsCompleted}");
}
catch (OperationCanceledException e)
{
    Console.WriteLine($"{nameof(OperationCanceledException)} " +
        $"thrown with message: {e.Message}");
}

Notes

We've looked at a number of ways to cancel a task. The soft cancellation, while successfully cancellation a task, has some limitations and the better option is to use a OperationCanceledException, facilitated by the ThrowIfCancellationRequested method.


References

Cancellation Tokens

Daily Drop 43: 01-04-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net anonymous with