12

I have that situation:

private Task LongRunningTask = /* Something */;

private void DoSomethingMore(Task previousTask) { }

public Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
{
    return LongRunningTask.ContinueWith(DoSomethingMore, cancellationToken);
}

In particular, the behavior that interests me here is detailed in MSDN's page about Continuation Tasks in the following terms:

A continuation goes into the Canceled state in these scenarios:

The code above works. However, I am in the process of converting as many as possible of my continuations to using the await keyword.

Is there an equivalent using await that would allow the continuation to be canceled before the awaited task completes?

svick
  • 236,525
  • 50
  • 385
  • 514
Jean Hominal
  • 16,518
  • 5
  • 56
  • 90
  • Well, I'd simply check the cancellation token manually, that's what happens anyway. That is, `if (!cancelled) await Task(); if (!cancelled) await Task2(); ...` And of course, you can pass the token to the method as well (where it can also be handled any way you want). – Luaan Jan 09 '14 at 10:42
  • @Luaan: Your comment is conceptually the same as [Francois Nel's answer](http://stackoverflow.com/a/21018024/113158), which does not resolve my question. – Jean Hominal Jan 09 '14 at 13:13
  • Are you sure that it works any differently when using ContinueWith? I thought that you can only cancel between tasks as well, and searching through Task source codes seems to support that - there's no way to cancel the task itself (unless you're handling the cancellation yourself inside the task), since it all depends on cooperative multi-tasking, not pre-emptive. At some point, the Task class simply launches your delegate, and it can't do anything until you return. Have you actually tried whether there is a difference between `await` and `ContinueWith` with cancellation? – Luaan Jan 09 '14 at 13:34
  • For example, you can await HttpClient with cancellation simply by doing `await client.GetAsync(uri, cancellationToken);`. So unless there's some hidden magic in the `Action` delegate itself (which is "implemented" outside of managed code, so I can't say for sure) or the ExecutionContext, you have to support cancellation "manually" inside your method - by having a cancellation token parameter which you use as usual. – Luaan Jan 09 '14 at 13:48
  • Also, don't forget that `await` and `async` use the same `Task.ContinueWith` method to do the continuations, so there's little reason to believe that it would work in a significantly different way. See also - http://msdn.microsoft.com/en-us/library/dd997364.aspx and http://www.microsoft.com/en-us/download/details.aspx?id=19957 – Luaan Jan 09 '14 at 14:02
  • @Luaan: I am not talking about cancelation inside of the continuation. I am talking about cancelation in the TPL system that schedules the continuation. The MSDN bit that I cite explicitly says that, in essence, the continuation will not be scheduled if cancelation is set before the continuation becomes eligible for scheduling. – Jean Hominal Jan 09 '14 at 14:11
  • @Luaan: And yes, I know that `await` and `async` use one of the `Task.ContinueWith` method - what I am asking is precisely about mapping one functionality provided by `Task.ContinueWith` that does not seem to have an `await` friendly equivalent. – Jean Hominal Jan 09 '14 at 14:12
  • Ah, I finally get it now :) Have you tried something like this? `await new Task(continuation, cancellationToken);` – Luaan Jan 09 '14 at 14:27
  • @Luaan: If you think you have a working solution, you are free to propose an answer. :) – Jean Hominal Jan 09 '14 at 17:54
  • See http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx – Matt Smith Oct 10 '14 at 00:00

4 Answers4

6

The following should do it, albeit it looks a bit awkward:

private Task LongRunningTask = /* Something */;

private void DoSomethingMore() { }

public async Task IndependentlyCancelableSuccessorTask(
    CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    var tcs = new TaskCompletionSource<bool>();
    using (cancellationToken.Register(() => tcs.TrySetCanceled()))
        await Task.WhenAny(LongRunningTask, tcs.Task);

    cancellationToken.ThrowIfCancellationRequested();
    DoSomethingMore();
}

[UPDATE] Following svick's suggestion, here it is shaped as a helper, based on Stephen Toub's Implementing Then with Await pattern:

public static class TaskExt
{
    /// <summary>
    /// Use: await LongRunningTask.Then(DoSomethingMore, cancellationToken)
    /// </summary>
    public static async Task Then(
        this Task antecedent, Action continuation, CancellationToken token)
    {
        await antecedent.When(token);
        continuation();
    }

    /// <summary>
    /// Use: await LongRunningTask.When(cancellationToken)
    /// </summary>
    public static async Task When(
        this Task antecedent, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<Empty>();
        using (token.Register(() => tcs.TrySetCanceled()))
            await Task.WhenAny(antecedent, tcs.Task);

        token.ThrowIfCancellationRequested();
    }

    struct Empty { };
}

Perhaps, the first ThrowIfCancellationRequested() is redundant, but I haven't thoroughly considered all edge cases.

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I think you should somehow encapsulate this into a helper method. That way, the actual code won't look awkward. – svick Jan 09 '14 at 12:05
  • @svick, I've re-factored it as helper, is that something that you meant? – noseratio Jan 09 '14 at 12:46
  • @Noseratio: While I like this answer conceptually, the implementation of the `Then` helper at this time has the issue that the awaiting code no longer decides where the continuation is executed. I think you should remove the call to `continuation()` and simply return the antecedant task. – Jean Hominal Jan 09 '14 at 13:05
  • @JeanHominal, how about adding `Task.When` in addition to `Then`? You can use `When` in the way you described. Check out the update. – noseratio Jan 09 '14 at 13:14
  • @JeanHominal, returning the antecedent task would make the signature be `Task`. This is a bit ugly, IMO, with a mental picture: `await await LongRunningTask.When(token)`. Instead, you can just use the original `LongRunningTask` task directly: `await LongRunningTask.When(token); DoSomethingMore(LongRunningTask);` – noseratio Jan 09 '14 at 13:26
  • @Noseratio: You are right, it would not work to return the antecedant task should not be returned. However, now that I have played a bit with how it could be implemented, and how it could be implemented without using the `async` keyword, it has come to my attention that the second call to `token.ThrowIfCancellationRequested` should be replaced with an `await` call on the task returned by the `Task.WhenAny`. As it currently stands, no exception will be raised if it is the `antecedent` task that faults or is canceled.) (Writing a version of `When` with a result helped me in clarifying things.) – Jean Hominal Jan 09 '14 at 14:43
  • @Noseratio: I have written my proposal for an implementation in [another answer](http://stackoverflow.com/a/21023565/113158) on this thread. Here are the reasons why I wrote it instead of trying to fix these issues by commenting: 1. correctly handling the case where `antecedent` completes first; 2. having a version of the helper that returns a `Task`; 3. use `ConfigureAwait(false)` to avoid marshaling the rest of the method to a `SynchronizationContext` as it is not necessary; 4. avoid most of the machinery if the token cannot be canceled; 5. (nitpick) remove unnecessary `Empty` type; – Jean Hominal Jan 09 '14 at 15:07
  • 2
    @JeanHominal, no problem. Glad you you liked the concept, although apparently it would have taken quite a few more iterations for me to satisfy *all* of your requirements :) BTW, I do like `struct Empty` with `TaskCompletionSource`, it wasn't invited by me. – noseratio Jan 09 '14 at 20:14
  • @Horatio: Even if you do not apply all of the changes I made in my answer, I would like you to at least consider doing the modification to handle correctly the case where `antecedent` finishes first (e.g. by removing your last `ThrowIfCancellationRequested()` call and adding an `await` before the `Task.WhenAny` call). – Jean Hominal Jan 09 '14 at 21:41
  • @JeanHominal, if I remove the last `ThrowIfCancellationRequested()`, wouldn't that be in conflict with this quote from your question: *When the continuation was passed a System.Threading.CancellationToken as an argument and the IsCancellationRequested property of the token is true before the continuation runs. In such a case, the continuation does not start and it transitions to the Canceled state.* Also, there *is* an await before `Task.WhenAny` in my code, am I missing something? – noseratio Jan 10 '14 at 01:49
  • @Noseratio: The return type from `WhenAny` is `Task` - that is, use `await` once to get the finished task asynchronously, and use `await` a second time to asynchronously get the result of the finished task. So, what I am proposing is to write `await await WhenAny`, and the net result will be, first, that you asynchronously get the finished task (either the antecedent, or the cancellation task), and you await on that, which will raise the exception of the finished task. If it is the cancellation task that completed (in the `Canceled` state), then awaiting on it will raise the exception. – Jean Hominal Jan 10 '14 at 06:32
  • Well, what I am proposing is to write `await await WhenAny`, *and* remove the last `ThrowIfCancellationRequested`. – Jean Hominal Jan 10 '14 at 06:38
  • @JeanHominal, why should I care about **the result of `WhenAny`** at all? `await WhenAny` doesn't re-throw in case either of the tasks fails (throws or cancelled), it just **finishes successfully**. Given that, if cancellation is requested before `antecedent` finishes, or before I invoke `continuation`, the last `ThrowIfCancellationRequested()` will throw, same as `ContinueWith` in your question would. Otherwise, `continuation` will be invoked, regardless of the `antecedent` completion status - again, same as `ContinueWith`. Right? – noseratio Jan 10 '14 at 07:14
  • Now, I don't even want to expose `tcs.Task` outside, it's a very internal object to the pattern. By analogy, the `DoSomethingMore` delegate in your question is supposed to only see `LongRunningTask`. Thus, why would I do `await await WhenAny()` then? It may throw and expose the `tcs.Task` via `Exception` object. Anyhow, tastes differ. It's been an interesting discussion - thanks, but I'd like to move on with other questions. – noseratio Jan 10 '14 at 07:15
  • Back here to check if the down-voter of my answer left any comments, but there is none... Ah, it was un-upvoted, got it. – noseratio Jan 10 '14 at 09:13
  • @Noseratio: I had forgotten that a continuation registered with `ContinueWith` will continue even if `antecedent` is faulted or canceled, so I now see that your behavior is closer to my original question's code. Thank you for the discussion. – Jean Hominal Jan 10 '14 at 09:30
  • 1
    @Noseratio: Also, now I think I understand the role of the `Empty` struct, that is, someone that receives the `Task` cannot cast it to `Task` because `Empty` is not visible. – Jean Hominal Jan 10 '14 at 14:13
  • 1
    @Noseratio, I've added an answer (that is an off-shoot of yours). I'd be interested to hear any comments you have, as I respect your judgement. – Matt Smith Oct 09 '14 at 15:41
3

While this answer is conceptually the same as Noseratio's, I am not satisfied by a few details of the implementation, and as such am publishing my proposed implementation of the helper so that it can be commented on by other people on this question.

public static async Task<TResult> WhenNotCanceled<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
{
    if (!cancellationToken.CanBeCanceled) {
        return await mainTask.ConfigureAwait(false);
    }

    cancellationToken.ThrowIfCancellationRequested();

    Task<TResult> completedTask;

    var cancellationTaskSource = new TaskCompletionSource<TResult>();
    using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
        completedTask = await Task.WhenAny(mainTask, cancellationTaskSource.Task).ConfigureAwait(false);

    cancellationToken.ThrowIfCancellationRequested();
    return await completedTask.ConfigureAwait(false);
}

public static async Task WhenNotCanceled(this Task mainTask, CancellationToken cancellationToken)
{
    if (!cancellationToken.CanBeCanceled) {
        await mainTask.ConfigureAwait(false);
        return;
    }

    cancellationToken.ThrowIfCancellationRequested();

    Task completedTask;

    var cancellationTaskSource = new TaskCompletionSource<object>();
    using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
        completedTask = await Task.WhenAny(mainTask, cancellationTaskSource.Task).ConfigureAwait(false);

    cancellationToken.ThrowIfCancellationRequested();
    await completedTask.ConfigureAwait(false);
}

Async pattern without cancel:

public async Task IndependentlyCancelableSuccessorTask()
{
    await LongRunningTask;
    DoSomethingMore();
}

Async pattern with cancel and WhenNotCanceled:

public async Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
{
    await LongRunningTask.WhenNotCanceled(cancellationToken);
    DoSomethingMore();
}
Jean Hominal
  • 16,518
  • 5
  • 56
  • 90
  • 1
    If you're concerned about capturing synchronization context, I'd suggest that you do `using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)`. – noseratio Jan 09 '14 at 22:15
  • @Noseratio: Of course I am concerned about capturing the synchronization context - for me, the `Task` returned from a function must be able to be waited synchronously as well as asynchronously - and if it is waited synchronously, then it is actually the caller's synchronization context that gets captured, which can easily trigger a deadlock if running on the UI thread. Besides, getting back to the synchronization context does have a cost, and why pay it when you don't need to? – Jean Hominal Jan 10 '14 at 06:48
  • One more point about your code: it may **expose the internal `TaskCompletionSource.Task`** to the outer caller. It's available on `Exception` when `await completedTask` throws (if cancelled). IMO, it should remain private to the pattern. Also, I believe the second `await completedTask` is redundant, as discussed in the comments to my answer. That was a job for `token.ThrowIfCancellationRequested()` in my version. So, I take liberty to down-vote this. – noseratio Jan 10 '14 at 09:23
  • @Noseratio: I guess you are right, that the cancellation task should not be leaked outside. Did an edit with that modification, and added the targeted usage pattern, that is, I want to have the same behavior as `await`, where the faults and cancellation inside of the main task are propagated outside. – Jean Hominal Jan 10 '14 at 09:54
  • If that's your goal, then the code does what it's meant to do. The boss calls the shots, +1 :) – noseratio Jan 10 '14 at 10:15
1

My answer is only slightly different than @Jean Hominal's answer and incorporates @Noseratio's approach as well:

public static class TaskExtensionMethods
{
    public static Task<TResult> OrWhenCancelled<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
    {
        if (!cancellationToken.CanBeCanceled)
            return mainTask;

        return OrWhenCancelled_(mainTask, cancellationToken);
    }

    private static async Task<TResult> OrWhenCancelled_<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
    {
        Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
        await Task.WhenAny(mainTask, cancellationTask).ConfigureAwait(false);

        cancellationToken.ThrowIfCancellationRequested();
        return await mainTask;
    }

    public static Task OrWhenCancelled(this Task mainTask, CancellationToken cancellationToken)
    {
        if (!cancellationToken.CanBeCanceled)
            return mainTask;

        return OrWhenCancelled_(mainTask, cancellationToken);
    }

    private static async Task OrWhenCancelled_(this Task mainTask, CancellationToken cancellationToken)
    {
        Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
        await Task.WhenAny(mainTask, cancellationTask).ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();
        await mainTask;
    }
}

Discussion:

  • All of the solutions (including this one), do not correctly handle the case where the original ContinueWith specified a TaskScheduler. Specifically, consider a TaskScheduler created TaskScheduler.FromCurrentSynchronizationContext for usage in UI scenarios. In that case, with the original ContinueWith approach you were guaranteed that the cancellation token was checked prior to running the delegate but after already getting on to Main thread (see this answer). That is, the old approach has the nice effect of checking the Cancellation token "one last time" on the main thread prior to considering the result of the task (i.e. trumping whether the main task finished or faulted). This means that in addition to using these extension methods, the new code must wrap its await in a try/finally to do its final check of the CancellationToken :(. See this question.

  • @Noseratio's solution could handle the above issue (if needed), but it has the downside of requiring that continuation be placed into a delegate. In my opinion, this defeats one of the big advantages of converting to using await: the code doesn't end up in a delegate, it is just after an await and reads like normal sequential code.

Notes:

  • I wish I could have specified that the empty lambda never runs (i.e. instead of only running on cancellation), but the .ContinueWith method doesn't allow that. So, I (mostly arbitrarily chose OnlyOnCancelled)
Community
  • 1
  • 1
Matt Smith
  • 17,026
  • 7
  • 53
  • 103
  • 1
    Hi Matt, I might have more thoughs later, so far one thing. Do you think `Task cancellationTask = mainTask.ContinueWith(t => { }, cancellationToken, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.Default);` can simply be replaced with `Task.Delay(Timeout.Infinite, cancellationToken)`? The latter is implemented quite efficiently by CLR. – noseratio Oct 09 '14 at 21:07
  • I also have another concern about `return await mainTask.ConfigureAwait(false);`. Depending on the nature of `mainTask`, this may incur redundant context/thread switches: http://stackoverflow.com/q/22672984/1768303 – noseratio Oct 09 '14 at 21:09
  • Also, should we consider using [`LazyCancellation`](http://blogs.msdn.com/b/pfxteam/archive/2012/09/22/new-taskcreationoptions-and-taskcontinuationoptions-in-net-4-5.aspx) with `ContinueWith`? Or just ignore the (possibly) pending `mainTask` if the cancellation has been triggered? – noseratio Oct 09 '14 at 21:15
  • @Noseratio, regarding the context/thread switches for the line `return await mainTask.ConfigureAwait(false)`. The mainTask will *always* be completed at this point, so won't the await keyword just see that it is completed and continue on without any context switches? Or are there cases where await will not continue on for a completed task? – Matt Smith Oct 09 '14 at 21:42
  • @Noseratio, regarding `LazyCancellation`: I think that is the exact opposite of the behavior we would want: `LazyCancellation` would mean that the cancellationTask wouldn't be cancelled until the LongRunning task (mainTask) was also completed. We want that cancellationTask to be completed as soon as possible after the cancellationTokenSource is Cancelled. I think we do want to ignore the pending mainTask (it will finish, fault, or cancel--we don't care)--thats the behavior of the original .ContinueWith code. – Matt Smith Oct 09 '14 at 21:46
  • 1
    @Noseratio, I tried out the `Task.Delay` approach, and it works great. I find that *much* more readable than all the approaches thus far. Edited my answer. Thanks! – Matt Smith Oct 09 '14 at 21:59
  • 1
    Matt, if `mainTask` will *always* be completed, why use `mainTask.ConfigureAwait(false)` at all? :) I agree about `LazyCancellation` but now I feel we're all reinventing the wheel here, as Stephen Toub did it already :) ["How do I cancel non-cancelable async operations?"](http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx) – noseratio Oct 09 '14 at 22:03
  • 1
    @Noseratio, Ah, yes, the ConfigureAwait(false) is superfluous (I'll edit and remove). And *yes* I wasn't aware of that article. The only behavioral difference between his approach and our approach is that in the case of the task completing and the cancellation token completing around the same time, our solution prefers to honor the cancellation. I somewhat still like the readability of the Task.Delay better. And I really wish there had been a ConfigureAwait that took a CancellationToken as it would have made conversion of this type of code have an exact equivalent. – Matt Smith Oct 09 '14 at 23:56
  • I like `Task.Delay` too, been using it since [this](http://stackoverflow.com/a/23473779/1768303). – noseratio Oct 10 '14 at 00:02
  • 1
    @Noseratio, look at this version of WithCancellation: http://stackoverflow.com/a/26305788/495262 – Matt Smith Oct 10 '14 at 18:53
0

This answer comes from @Servy from this answer (with modifications):

public static Task WithCancellation(this Task task,
CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

public static Task<T> WithCancellation<T>(this Task<T> task,
CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
Community
  • 1
  • 1
Matt Smith
  • 17,026
  • 7
  • 53
  • 103