187

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()
{
}
Carlo
  • 25,602
  • 32
  • 128
  • 176
  • Just found [this article](https://johnthiriet.com/cancel-asynchronous-operation-in-csharp/) which helped my understand the various ways to cancel. – Uwe Keim Dec 03 '18 at 09:33

4 Answers4

273

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;
}
Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 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 '12 at 02: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 '12 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. – Stephen Cleary Apr 14 '12 at 00:01
  • 1
    Ah I see. So the best way to catch the ProcessCancelledException is by wrapping the 'await' in a try / catch? Sometimes I get the AggregatedException and I can't handle that. – Carlo Apr 14 '12 at 00:14
  • 4
    Right. I [recommend](http://nitoprograms.blogspot.com/2012/02/async-and-await.html) that you never use `Wait` or `Result` in `async` methods; you should always use `await` instead, which unwraps the exception correctly. – Stephen Cleary Apr 14 '12 at 00:17
  • I see... that's how I did it, but I keep getting that AggregatedException and it just halts the application... I'm pretty stuck there. I guess I'll just start testing another feature... – Carlo Apr 14 '12 at 00:19
  • In `Task.Run(action)`, why does the cancellation token need to be given to *both* the action *and* to `Task.Run()` itself? It's obvious why the action needs the token, but not so much (to me) why `Task.Run()` needs it as well. – Cristian Diaconescu Jan 28 '14 at 08:14
  • 1
    @CristiDiaconescu: In this example, there's no point to passing it to `Task.Run`. But if a method has a token that it doesn't control (which is the usual case), passing it to `Task.Run` will prevent the actual task from starting if the token is already canceled. – Stephen Cleary Jan 28 '14 at 13:17
  • Got it. So this would make sense if the Task.Run() is e.g. a continuation to some previous task. – Cristian Diaconescu Jan 28 '14 at 13:23
  • @CristiDiaconescu: Or if the `TryTask` method took the token as an argument. – Stephen Cleary Jan 28 '14 at 13:26
  • For the extended explanation see https://msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx – Ilya Serbis Jun 20 '16 at 20:44
  • @StephenCleary sorry to trouble you, having bought and read your book and read numerous posts online and SO.. you'd think I would know better but. Is this a terrible thing to do: http://paste.ofcode.org/q23Yefcnzday29EcCH28NN – peteski Jun 30 '16 at 13:36
  • 1
    @peteski22: Yes, there's a number of issues with that. It looks like you'd want the [`Task.WaitAsync` extension that I have in my `Nito.AsyncEx.Tasks` library](https://github.com/StephenCleary/AsyncEx.Tasks). – Stephen Cleary Jun 30 '16 at 14:02
  • 15
    Just curious, is there a reason why none of the examples use `CancellationToken.IsCancellationRequested` and instead suggest throwing exceptions? –  Aug 08 '16 at 16:25
  • 2
    @ThyArtIsCode: If a method takes a `CancellationToken`, then the expected behavior is to throw an `OperationCanceledException` if the operation is cancelled. That's the standard behavior. Any other behavior (like returning a sentinel value) would be unusual and need explicit documentation. – Stephen Cleary Aug 08 '16 at 16:41
  • Would it be a good idea to use `CancellationToken` as an `IAsyncDisposable`? I have a method that disables a feature in the database. I want to implement it in a way that ensures that after this feature is disabled and some work is done, it's enabled back. Since the requests to the DB are async, I thought about using a `CancellationToken`, and enable back the DB feature on cancellation. But the cancellation action is also async. Is there an `await cancellationTokenSource.CancelAsync()`? Any alternatives? – Shimmy Weitzhandler Jan 03 '19 at 23:51
  • @Shimmy: I would think that an `IAsyncDisposable` would be sufficient. You would want the feature re-enabled regardless of whether it's cancelled, correct? – Stephen Cleary Jan 04 '19 at 14:29
  • @StephenCleary Exactly, I want to use the cancellation to signal the `Task` that disabling the database feature is over, thus reenabling it. Anyway can't find `IAsyncDisposable` in .NET Core 2.2, is it a version 3.0 feature? A NuGet package? – Shimmy Weitzhandler Jan 06 '19 at 01:05
  • 1
    `IAsyncDisposable` is 3.0 (prerelease). – Stephen Cleary Jan 06 '19 at 02:04
  • @StephenCleary, what should I use in 2.2? Should I execute the cancellation task synchronously? Any decent workarounds? – Shimmy Weitzhandler Jan 07 '19 at 03:17
  • 1
    @Shimmy: There's a few workarounds. Personally, I would prefer a design where the remote system *assumes* cancellation if, say, the socket is abruptly closed without a "commit" message. Then you can handle cancellation on the client side synchronously: just close the socket connection. If this isn't possible, you can create your own pattern (without compiler support, of course): https://blog.stephencleary.com/2013/03/async-oop-6-disposal.html – Stephen Cleary Jan 07 '19 at 14:10
  • Dont use Task.Run to make async https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-5.0#avoid-blocking-calls – Willimar Sep 06 '21 at 16:51
47

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());
sonatique
  • 1,465
  • 13
  • 8
  • 2
    It looks very tricky... as whole async-await implementation. I do not think that such constructions make source code more readable. – Maxim Oct 30 '17 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. – astrowalker Mar 14 '18 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 '18 at 18:20
  • 1
    Actually all it takes is nest it in `using`. – astrowalker Mar 15 '18 at 07:06
  • @astrowalker ;-) yes you're right actually. In this case this is the much simpler solution! However, if you wish to return Task.WhenAny directly (without await) then you need something else. I say this because I once hit a refactoring issue like this: before I had using...await. I then removed the await (and the async on the function) since it was the only one, without noticing that I was completely breaking the code. The resulting bug was difficult to find. I am thus reluctant to use using() together with async/await. I feel the Dispose pattern doesn't go well with async things anyway... – sonatique Mar 15 '18 at 11:15
  • I was hit by this as well ;-), but if you consider that returning Task can hit you also if you have some asserts in the header of the method the conclusion is -- it is better not to return Task directly. Ever. `await` (thus indirectly returning Task) is the way to go. – astrowalker Mar 15 '18 at 14:32
  • Sadly, the newest version of Stephen Cleary's AsyncEx (currently: 5.0.0) does not contain the CancellationTokenExtensions any more. For that, you have to install version 4.0.1. – J S Jun 12 '19 at 12:22
  • 2
    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. – quetzalcoatl Dec 03 '21 at 18:20
24

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.

kjbartel
  • 10,381
  • 7
  • 45
  • 66
  • 3
    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 02 '19 at 06: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 05 '19 at 09: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 06 '19 at 01:03
  • 1
    @Miral Thanks. I've updated to reflect this caveat. – kjbartel Aug 06 '19 at 07:09
  • Sadly this don't works with methods like ' NetworkStream.WriteAsync'. – Zeokat Mar 22 '20 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.

EluciusFTW
  • 2,565
  • 6
  • 42
  • 59
Smeegs
  • 9,151
  • 5
  • 42
  • 78