3

How could you explain the following behaviour:

await Task.Run(() => { }).ContinueWith(async prev =>
{
    Console.WriteLine("Continue with 1 start");
    await Task.Delay(1000);
    Console.WriteLine("Continue with 1 end");
}).ContinueWith(prev =>
{
    Console.WriteLine("Continue with 2 start");
});

Why will we get “Continue with 2 start” before “Continue with 1 end”?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Vladyslav Furdak
  • 1,765
  • 2
  • 22
  • 46
  • ContinueWith doesn't work with `async/await` nor does it know about them. `await` is a *replacement* for `ContinueWith`. – Panagiotis Kanavos Nov 14 '19 at 11:19
  • 1
    Why are you using `ContinueWith` instead of `await`? – Panagiotis Kanavos Nov 14 '19 at 11:20
  • 1
    You might be better off doing without `ContinueWith` and just use `await`. _["await leads to much simpler code. Additionally, as noted by Servy in comments, awaiting a task will "unwrap" aggregate exceptions which usually leads to simpler error handling"](https://stackoverflow.com/a/18965299/585968)_ –  Nov 14 '19 at 11:20
  • @PanagiotisKanavos But, how to explain that behavior? Why await returns task for anonymous method. It's just an investigation. – Vladyslav Furdak Nov 14 '19 at 11:21
  • 2
    Because you don't await your first continuation. Thus your first continuation is started, prints "Continue with 1 start", calls Task.Delay, schedules the remaining code (printing "Continue with 1 end") as another implicit continuation, returns, which kicks off continuation 2. Your implicit continuation is then triggered after Task.Delay ends = after one second. – ckuri Nov 14 '19 at 11:21
  • 2
    Because you created an `async void` method that is never awaited by anything – Panagiotis Kanavos Nov 14 '19 at 11:21
  • @ckuri the continuation *is* awaited. What it does though, is fire off a task that nobody is going to await – Panagiotis Kanavos Nov 14 '19 at 11:22

2 Answers2

4

ContinueWith doesn't know anything about async and await. It doesn't expect a Task result so doesn't await anything even if it gets one. await was created as a replacement for ContinueWith.

The cause of the problem is that ContinueWith(async prev => creates an implicit async void delegate. ContinueWith has no overload that expects a Task result, so the only valid delegate that can be created for ContinueWith(async prev => question's code is :

async void (prev) 
{
    Console.WriteLine(“Continue with 1 start”);
    await Task.Delay(1000);
    Console.WriteLine(“Continue with 1 end”);
}

async void methods can't be awaited. Once await Task.Delay() is encountered, the continuation completes, the delegate yields and the continuation completes. If the application exits, Continue with 1 end may never get printed. If the application is still around after 1 second, execution will continue.

If the code after the delay tries to access any objects already disposed though, an exception will be thrown.

If you check prev.Result's type, you'll see it's a System.Threading.Tasks.VoidTaskResult. ContinueWith just took the Task generated by the async/await state machine and passed it to the next continuation

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
3

The code below is equivalent to your example, with variables explicitly declared, so that it's easier to see what's going on:

Task task = Task.Run(() => { });

Task<Task> continuation1 = task.ContinueWith(async prev =>
{
    Console.WriteLine("Continue with 1 start");
    await Task.Delay(1000);
    Console.WriteLine("Continue with 1 end");
});

Task continuation2 = continuation1.ContinueWith(prev =>
{
    Console.WriteLine("Continue with 2 start");
});

await continuation2;
Console.WriteLine($"task.IsCompleted: {task.IsCompleted}");
Console.WriteLine($"continuation1.IsCompleted: {continuation1.IsCompleted}");
Console.WriteLine($"continuation2.IsCompleted: {continuation2.IsCompleted}");

Console.WriteLine($"continuation1.Unwrap().IsCompleted:" +
    $" {continuation1.Unwrap().IsCompleted}");

await await continuation1;

Output:

Continue with 1 start
Continue with 2 start
task.IsCompleted: True
continuation1.IsCompleted: True
continuation2.IsCompleted: True
continuation1.Unwrap().IsCompleted: False
Continue with 1 end

The tricky part is the variable continuation1, that is of type Task<Task>. The ContinueWith method does not unwrap automatically the Task<Task> return values like Task.Run does, so you end up with these nested tasks-of-tasks. The outer Task's job is just to create the inner Task. When the inner Task has been created (not completed!), then the outer Task has been completed. This is why the continuation2 is completed before the inner Task of the continuation1.

There is a built-in extension method Unwrap that makes it easy to unwrap a Task<Task>. An unwrapped Task is completed when both the outer and the inner tasks are completed. An alternative way to unwrap a Task<Task> is to use the await operator twice: await await.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104