Vladyslav Furdak
Vladyslav Furdak

Reputation: 1835

Async in Task ContinueWith behavior?

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”?

Upvotes: 3

Views: 5302

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43409

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.

Upvotes: 3

Panagiotis Kanavos
Panagiotis Kanavos

Reputation: 131219

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

Upvotes: 4

Related Questions