190

I'm playing with these Windows 8 WinRT tasks, and I'm trying to cancel a task using the method below, and it works to some point. The CancelNotification method DOES get called, which makes you think the task was cancelled, but in the background the task keeps running, then after it's completed, the status of the Task is always completed and never cancelled. Is there a way to completely halt the task when it's cancelled?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}
2

4 Answers 4

276

Read up on Cancellation (which was introduced in .NET 4.0 and is largely unchanged since then) and the Task-Based Asynchronous Pattern, which provides guidelines on how to use CancellationToken with async methods.

To summarize, you pass a CancellationToken into each method that supports cancellation, and that method must check it periodically.

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}
22
  • 2
    Wow great info! That worked perfectly, now I need to figure out how to handle the exception in the async method. Thanks man! I'll read the stuff you suggested.
    – Carlo
    Apr 13, 2012 at 2:48
  • 4
    Hey man, is there a way to do it if I don't have access to the slow method? For example, suppose slowFunc was in a blackbox and you only have access to call the method, but not to modify anything within it?
    – Carlo
    Apr 13, 2012 at 19:12
  • 12
    No. Most long-running synchronous methods have some way to cancel them - sometimes by closing an underlying resource or calling another method. CancellationToken has all the hooks necessary to interop with custom cancellation systems, but nothing can cancel an uncancelable method. Apr 14, 2012 at 0:01
  • 4
    Right. I recommend that you never use Wait or Result in async methods; you should always use await instead, which unwraps the exception correctly. Apr 14, 2012 at 0:17
  • 18
    Just curious, is there a reason why none of the examples use CancellationToken.IsCancellationRequested and instead suggest throwing exceptions?
    – user1618054
    Aug 8, 2016 at 16:25
49

Or, in order to avoid modifying slowFunc (say you don't have access to the source code for instance):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

You can also use nice extension methods from https://github.com/StephenCleary/AsyncEx and have it looks as simple as:

await Task.WhenAny(task, source.Token.AsTask());
8
  • 3
    It looks very tricky... as whole async-await implementation. I do not think that such constructions make source code more readable.
    – Maxim
    Oct 30, 2017 at 17:58
  • 1
    Thank you, one note -- registering token should be later disposed, second thing -- use ConfigureAwait otherwise you can be hurt in UI apps. Mar 14, 2018 at 14:47
  • @astrowalker : yes indeed registration of token shall better be unregisterd (disposed). This can be done inside the delegate that is passed to Register() by calling dispose on the object that is returned by Register(). However since "source" token is only local in this case, everything will be cleared anyway...
    – sonatique
    Mar 14, 2018 at 18:20
  • 1
    Actually all it takes is nest it in using. Mar 15, 2018 at 7:06
  • 3
    I'm pretty much sure that this example does not in fact interrupt in any way that unmodified-slow-func. All it does is starting a new Task in parallel with a timer, and waits until whatever completes first. If it times out, it indeed stops awaiting on the final await-task-whenany, but it also allows the slow-running function to keep running, which potentially wastes resources, and which is far from what was requested in the question. The question was about halting the task completely. I'd say this is a critical divergence from the topic, please add a word or two about it to the answer. Dec 3, 2021 at 18:20
25

One case which hasn't been covered is how to handle cancellation inside of an async method. Take for example a simple case where you need to upload some data to a service get it to calculate something and then return some results.

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

If you want to support cancellation then the easiest way would be to pass in a token and check if it has been cancelled between each async method call (or using ContinueWith). If they are very long running calls though you could be waiting a while to cancel. I created a little helper method to instead fail as soon as canceled.

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

So to use it then just add .WaitOrCancel(token) to any async call:

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

Note that this will not stop the Task you were waiting for and it will continue running. You'll need to use a different mechanism to stop it, such as the CancelAsync call in the example, or better yet pass in the same CancellationToken to the Task so that it can handle the cancellation eventually. Trying to abort the thread isn't recommended.

5
  • 4
    Note that while this cancels awaiting for the task, it doesn't cancel the actual task (so eg. UploadDataAsync may continue in the background, but once it completes it won't make the call to CalculateAsync because that part already stopped waiting). This may or may not be problematic for you, especially if you want to retry the operation. Passing the CancellationToken all the way down is the preferred option, when possible.
    – Miral
    Aug 2, 2019 at 6:18
  • 2
    @Miral that is true however there are many async methods which do not take cancellation tokens. Take for example WCF services, which when you generate a client with Async methods will not include cancellation tokens. Indeed as the example shows, and as Stephen Cleary also noted, it is assumed that long running synchronous tasks have some way to cancel them.
    – kjbartel
    Aug 5, 2019 at 9:29
  • 2
    That's why I said "when possible". Mostly I just wanted this caveat to be mentioned so that people finding this answer later don't get the wrong impression.
    – Miral
    Aug 6, 2019 at 1:03
  • 1
    @Miral Thanks. I've updated to reflect this caveat.
    – kjbartel
    Aug 6, 2019 at 7:09
  • Sadly this don't works with methods like ' NetworkStream.WriteAsync'.
    – Zeokat
    Mar 22, 2020 at 12:34
6

I just want to add to the already accepted answer. I was stuck on this, but I was going a different route on handling the complete event. Rather than running await, I add a completed handler to the task.

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

Where the event handler looks like this

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

With this route, all the handling is already done for you, when the task is cancelled it just triggers the event handler and you can see if it was cancelled there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.