Jeff Walker Code Ranger
Jeff Walker Code Ranger

Reputation: 4926

Concise way to await a canceled Task?

I find myself writing code like this a lot:

try
{
    cancellationTokenSource.Cancel();
    await task.ConfigureAwait(false); // this is the task that was cancelled
}
catch(OperationCanceledException)
{
    // Cancellation expected and requested
}

Given that I requested the cancellation, it is expected and I'd really like the exception to be ignored. This seems like a common case.

Is there a more concise way to do this? Have I missed something about cancellation? It seems like there should be a task.CancellationExpected() method or something.

Upvotes: 8

Views: 3393

Answers (6)

Jeff Walker Code Ranger
Jeff Walker Code Ranger

Reputation: 4926

My final solution was to create an extension method as suggested by Matt Johnson-Pint. However, I return a boolean indicating whether the task was canceled as shown in Vasil Oreshenski's answer.

public static async Task<bool> CompletionIsCanceledAsync(this Task task)
{
    if (task.IsCanceled) return true;
    try
    {
        await task.ConfigureAwait(false);
        return false;
    }
    catch (OperationCanceledException)
    {
        return true;
    }
}

This method has been fully unit tested. I picked the name to be similar to the WaitForCompletionStatus() method in the ParallelExtensionsExtras sample code and the IsCanceled property.

Upvotes: 0

vasil oreshenski
vasil oreshenski

Reputation: 2836

If you are expecting the task to be cancelled BEFORE the await you should check the state of the cancellation token source.

if (cancellationTokenSource.IsCancellationRequested == false) 
{
    await task;
}

EDIT: As mentioned in the comments this won't do any good if the task is cancelled while awaited.


EDIT 2: This approach is overkill because it acquires additional resource - in hot path this may have performance hit. (i am using SemaphoreSlim but you can use another sync. primitive with the same success)

This is an extension method over existing task. The extension method will return new task which holds information if the original task was cancelled.

  public static async Task<bool> CancellationExpectedAsync(this Task task)
    {
        using (var ss = new SemaphoreSlim(0, 1))
        {
            var syncTask = ss.WaitAsync();
            task.ContinueWith(_ => ss.Release());
            await syncTask;

            return task.IsCanceled;
        }
    }

Here is a simple usage:

var cancelled = await originalTask.CancellationExpectedAsync();
if (cancelled) {
// do something when cancelled
}
else {
// do something with the original task if need
// you can acccess originalTask.Result if you need
}

How it works: Overall it waits for the original task to complete and returns information if was cancelled. The SemaphoraSlim is usually used to limit the access to some resource(expensive) but in this case i am using it to await until the original task has finished.

Notes: It does not returns the original task. So if you need something that has been returned from it you should inspect the original task.

Upvotes: -3

Theodor Zoulias
Theodor Zoulias

Reputation: 43400

There is a built-in mechanism, the Task.WhenAny method used with a single argument, but it's not very intuitive.

Creates a task that will complete when any of the supplied tasks have completed.

await Task.WhenAny(task); // await the task ignoring exceptions
if (task.IsCanceled) return; // the task is completed at this point
var result = await task; // can throw if the task IsFaulted

It is not intuitive because the Task.WhenAny is normally used with at least two arguments. Also it is slightly inefficient because the method accepts a params Task<TResult>[] tasks argument, so on every invocation an array is allocated in the heap.

Upvotes: 5

Enrico Massone
Enrico Massone

Reputation: 7338

The cancellation pattern available in C# in called cooperative cancellation.

This basically means that, in order to cancel any operation, there should be two actors which need to collaborate. One of them is the actor requesting the cancellation and the other is the actor listening to cancellation requests.

In order to implement this pattern you need an instance of CancellationTokenSource, which is an object that you can use in order to get an instance of CancellationToken. The cancellation is requested on the CancellationTokenSource instance and is propagated to the CancellationToken.

The following piece of code shows you this pattern in action and hopefully clarifies your doubt about cancellation:

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

namespace ConsoleApp2
{
  public static class Program
  {
    public static async Task Main(string[] args)
    {
      using (var cts = new CancellationTokenSource())
      {
        CancellationToken token = cts.Token;

        // start the asyncronous operation
        Task<string> getMessageTask = GetSecretMessage(token);

        // request the cancellation of the operation
        cts.Cancel();


        try
        {
          string message = await getMessageTask.ConfigureAwait(false);
          Console.WriteLine($"Operation completed successfully before cancellation took effect. The message is: {message}");
        }
        catch (OperationCanceledException)
        {
          Console.WriteLine("The operation has been canceled");
        }
        catch (Exception)
        {
          Console.WriteLine("The operation completed with an error before cancellation took effect");
          throw;
        }

      }
    }

    static async Task<string> GetSecretMessage(CancellationToken cancellationToken)
    {
      // simulates asyncronous work. notice that this code is listening for cancellation
      // requests
      await Task.Delay(500, cancellationToken).ConfigureAwait(false);
      return "I'm lost in the universe";
    }
  }
}

Pay attention to the comment and notice that all the 3 outputs for the program are possible.

There is no way to predict which of them will be the actual program result. The point is that when you await for the task completion you don't know what actually is going to happen. The operation may succeeds or fails before the cancellation took effect, or maybe the cancellation request can be observed by the operation before it runs to completion or fails for an error. From the calling code point of view, all these outcomes are possible and you have no way to make a guess. You need to handle all cases.

So, basically, your code is correct and you are handling the cancellation the way you should.

This book is an excellent reference to learn these things.

Upvotes: -1

Gabriel Luci
Gabriel Luci

Reputation: 40858

I assume whatever task is doing uses CancellationToken.ThrowIfCancellationRequested() to check for cancellation. That throws an exception by design.

So your options are limited. If task is an operation you wrote, you could make it not use ThrowIfCancellationRequested() and instead check IsCancellationRequested and end gracefully when needed. But as you know, the task's status won't be Canceled if you do that.

If it uses code you didn't write, then you don't have a choice. You'll have to catch the exception. You can use extension methods to avoid repeating code (Matt's answer), if you want. But you'll have to catch it somewhere.

Upvotes: 1

Matt Johnson-Pint
Matt Johnson-Pint

Reputation: 241450

I don't think there is anything built-in, but you could capture your logic in extension methods (one for Task, one for Task<T>):

public static async Task IgnoreWhenCancelled(this Task task)
{
    try
    {
        await task.ConfigureAwait(false);
    }
    catch (OperationCanceledException)
    {
    }
}

public static async Task<T> IgnoreWhenCancelled<T>(this Task<T> task)
{
    try
    {
        return await task.ConfigureAwait(false);
    }
    catch (OperationCanceledException)
    {
        return default;
    }
}

Then you can write your code simpler:

await task.IgnoreWhenCancelled();

or

var result = await task.IgnoreWhenCancelled();

(You might still want to add .ConfigureAwait(false) depending on your synchronization needs.)

Upvotes: 3

Related Questions