16

So, task.Wait() can be transformed to await task. The semantics are different, of course, but this is roughly how I would go about transforming a blocking code with Waits to an asynchronous code with awaits.

My question is how to transform task.Wait(CancellationToken) to the respective await statement?

Flexo
  • 87,323
  • 22
  • 191
  • 272
mark
  • 59,016
  • 79
  • 296
  • 580
  • possible duplicate of [Cancellation Token in await method](http://stackoverflow.com/questions/13822726/cancellation-token-in-await-method) – John Koerner Sep 02 '14 at 21:36
  • 4
    Nope, I have seen that one and did not find the answer there. Please, remove the duplication marker, unless explained why are they duplicate. – mark Sep 02 '14 at 21:37

4 Answers4

14

await is used for asynchronous methods/delegates, which either accept a CancellationToken and so you should pass one when you call it (i.e. await Task.Delay(1000, cancellationToken)), or they don't and they can't really be canceled (e.g. waiting for an I/O result).

What you can do however, is abandon* these kinds of tasks with this extension method:

public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted // fast-path optimization
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}

Usage:

await task.WithCancellation(cancellationToken);

* The abandoned task doesn't get cancelled, but your code behaves as though it has. It either ends with a result/exception or it will stay alive forever.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Wow, that is unexpectedly involved. – mark Sep 02 '14 at 21:44
  • 2
    @mark That's because `async-await` requires a different mindset. "regular" delegate tasks and "promise" async tasks, though they share a type, are quite different concepts... [Two Types of Task](http://blog.stephencleary.com/2014/04/a-tour-of-task-part-0-overview.html) – i3arnon Sep 02 '14 at 21:48
  • 1
    +1 for an elegant solution. I have a virtually identical extension method in my own code base, with the exception that I only bother registering a cancellation callback if `cancellationToken.CanBeCanceled` returns `true`. – Mike Strobel Sep 03 '14 at 12:47
  • I have some strange situation, where the cancellation callback is not called - please see the EDIT. – mark Sep 03 '14 at 16:58
  • @mark You have some strange and complicated code. You have a race condition in your `InternalTaskScheduler`. Try to run the same example with `Test(false)` in both calls, you'd be surprised at the result. Also try moving the `Thread.Sleep(1000)` to just after `ts.RunInline(t);`. My guess is that if it's fast enough, the thread swallows the `TaskCanceledException` in the empty `catch (OperationCanceledException)` – i3arnon Sep 03 '14 at 19:59
  • @mark I've also added a small example of usage to my answer. – i3arnon Sep 03 '14 at 20:05
  • @mark `Wow, that is unexpectedly involved.` You're quite right. it doesn't need to be this involved at all. – Servy Sep 03 '14 at 20:12
  • @I3arnon - it is a stripped down version of a code using a blocking collection to communicate work to threads. Obviously, it is a bad code... It is related to my other question - http://stackoverflow.com/questions/25632533/custom-thread-pool-supporting-async-actions – mark Sep 03 '14 at 21:37
  • @i3arnon Why did you add `TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default` patemeters and why it may be better compared to without them? – LukAss741 Jun 14 '20 at 16:26
  • @LukAss741 `TaskScheduler.Default` makes sure the continuation will run on a thread-pool thread. `ExecuteSynchronously` is useful for very short tasks. It executes on the same thread instead of re-scheduling on the thread-pool. – i3arnon Jun 15 '20 at 14:27
10

To create a new Task that represents an existing task but with an additional cancellation token is quite straightforward. You only need to call ContinueWith on the task, use the new token, and propagate the result/exceptions in the body of the continuation.

public static Task WithCancellation(this Task task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}

This allows you to write task.WithCancellation(cancellationToken) to add a token to a task, which you can then await.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Why not use `t.Result`? So the implementation would be the same for both `Task` and `Task`? – i3arnon Sep 03 '14 at 20:31
  • @I3arnon `Result` doesn't have the correct error propagation semantics. But the symmetry is nice, yes. – Servy Sep 03 '14 at 20:31
  • @Servy - great answer. I have never expected that passing the cancellation token to the continuation actually cancels the task itself. – mark Sep 04 '14 at 20:53
  • 2
    @mark It doesn't. It cancels the continuation which you are waiting on. – i3arnon Sep 05 '14 at 12:02
  • @I3arnon - that is what I have thought at first. But this is not what is happening. I have a task that would stay incomplete forever until one explicitly cancels, errors or sets the respective `TaskCompletionSource` instance. And yet, when I trigger the cancellation it does cancel the task. I have a program demonstrating this behavior, see EDIT 3 from http://stackoverflow.com/questions/25632533/custom-thread-pool-supporting-async-actions (you are quite familiar with that question :-)) – mark Sep 05 '14 at 14:49
  • @mark No, it's not. The method is not causing the underlying task to be cancelled, it's creating a new `Task` that will be canceled without actually cancelling the wrapped task. Your program is only ever actually inspecting the new wrapping task, not the composed task. – Servy Sep 05 '14 at 14:52
  • @mark **only** the continuation is canceled. See this 4 line example: https://gist.github.com/I3arnon/a1de39119f533ea0a943 – i3arnon Sep 05 '14 at 14:56
  • Hmm, I see, so the original task is continuing to run in the background with all the possible side effects. Seems like the only possible scenario for `Task.Wait(CancellationToken)` as well as for this extension is to check things and return back `Waiting` or `awaiting` the original task. We should not abandon it to run in the background "out of sight", sort of. Am I right? – mark Sep 05 '14 at 15:34
  • @mark You are correct that the original task is still doing whatever work it has set up to do, and you are quite right that, depending on the task, it may be dangerous to continue executing while that operation is running (it may also be safe, if it is not causing any side effects that would be observable). You should therefore use this approach with care. Of course, if you had some way of *actually* canceling the operation you wouldn't need to be doing this. – Servy Sep 05 '14 at 15:37
2

From https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#cancelling-uncancellable-operations

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);

    // This disposes the registration as soon as one of the tasks trigger
    using (cancellationToken.Register(state =>
    {
        ((TaskCompletionSource<object>)state).TrySetResult(null);
    },
    tcs))
    {
        var resultTask = await Task.WhenAny(task, tcs.Task);
        if (resultTask == tcs.Task)
        {
            // Operation cancelled
            throw new OperationCanceledException(cancellationToken);
        }

        return await task;
    }
}
R4cOOn
  • 2,340
  • 2
  • 30
  • 41
1

Here is another solution:

Task task;
CancellationToken token;
await Task.WhenAny(task, Task.Delay(Timeout.Infinite, token));

See this answer that talks about using Task.Delay() to create a Task from a CancellationToken. Here are the docs for Task.WhenAny and Task.Delay.

tleb
  • 4,395
  • 3
  • 25
  • 33
  • This creates a Task that will never complete. You would need to cancel the Task.Delay if the task returned from WhenAny is your original task. – mford Feb 27 '21 at 17:53