How to transform task.Wait(CancellationToken) to an await statement?
Asked Answered
Y

5

17

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?

Yablon answered 2/9, 2014 at 21:35 Comment(2)
possible duplicate of Cancellation Token in await methodEntertaining
Nope, I have seen that one and did not find the answer there. Please, remove the duplication marker, unless explained why are they duplicate.Yablon
G
11

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.

Gelatin answered 3/9, 2014 at 20:11 Comment(9)
Why not use t.Result? So the implementation would be the same for both Task<T> and Task?Limit
@I3arnon Result doesn't have the correct error propagation semantics. But the symmetry is nice, yes.Gelatin
@Gelatin - great answer. I have never expected that passing the cancellation token to the continuation actually cancels the task itself.Yablon
@Yablon It doesn't. It cancels the continuation which you are waiting on.Limit
@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 #25633033 (you are quite familiar with that question :-))Yablon
@Yablon 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.Gelatin
@Yablon only the continuation is canceled. See this 4 line example: gist.github.com/I3arnon/a1de39119f533ea0a943Limit
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?Yablon
@Yablon 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.Gelatin
L
15

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.

Limit answered 2/9, 2014 at 21:40 Comment(10)
Wow, that is unexpectedly involved.Yablon
@Yablon 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 TaskLimit
+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.Ammonal
I have some strange situation, where the cancellation callback is not called - please see the EDIT.Yablon
@Yablon 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)Limit
@Yablon I've also added a small example of usage to my answer.Limit
@Yablon Wow, that is unexpectedly involved. You're quite right. it doesn't need to be this involved at all.Gelatin
@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 - #25633033Yablon
@Limit Why did you add TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default patemeters and why it may be better compared to without them?Ontina
@Ontina 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.Limit
G
11

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.

Gelatin answered 3/9, 2014 at 20:11 Comment(9)
Why not use t.Result? So the implementation would be the same for both Task<T> and Task?Limit
@I3arnon Result doesn't have the correct error propagation semantics. But the symmetry is nice, yes.Gelatin
@Gelatin - great answer. I have never expected that passing the cancellation token to the continuation actually cancels the task itself.Yablon
@Yablon It doesn't. It cancels the continuation which you are waiting on.Limit
@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 #25633033 (you are quite familiar with that question :-))Yablon
@Yablon 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.Gelatin
@Yablon only the continuation is canceled. See this 4 line example: gist.github.com/I3arnon/a1de39119f533ea0a943Limit
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?Yablon
@Yablon 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.Gelatin
C
3

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;
    }
}
Commentate answered 2/8, 2022 at 12:47 Comment(0)
C
3

.NET 6 introduced Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken), plus related overloads and generics.

await task.WaitAsync(TimeSpan.FromSeconds(30), cts.Token);
Criner answered 24/1 at 10:27 Comment(0)
N
2

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.

Nondescript answered 16/12, 2020 at 14:33 Comment(1)
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.Diocese

© 2022 - 2024 — McMap. All rights reserved.