Reputation: 1835
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
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
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