What Happens While Waiting on a Task's Result

What happens while waiting on a Task's Result?

In Windows, all I/O is asynchronous. Synchronous APIs are just a convenient abstraction.

So, when you use HttpWebRequest.GetResponse, what actually happens is the I/O is started (asynchronously), and the calling thread (synchronously) blocks, waiting for it to complete.

Similarly, when you use HttpClient.PostAsync(..).Result, the I/O is started (asynchronously), and the calling thread (synchronously) blocks, waiting for it to complete.

I usually recommend people use await rather than Task.Result or Task.Wait for the following reasons:

  1. If you block on a Task that is the result of an async method, you can easily get into a deadlock situation.
  2. Task.Result and Task.Wait wrap any exceptions in an AggregateException (because those APIs are holdovers from the TPL). So error handling is more complex.

However, if you're aware of these limitations, there are some situations where blocking on a Task can be useful (e.g., in a Console application's Main).

What does accessing a Result on a Task before calling Wait actually do?

So does accessing any such property on Task act as a shortcut to calling Task.Wait()?

Yes.

From the docs:

Accessing the [Result] property's get accessor blocks the calling thread until the asynchronous operation is complete; it is equivalent to calling the Wait method.

However, your test doesn't do what you think it does.

Task.Delay(..) returns a Task which completes after the specified amount of time. It doesn't block the calling thread.

So () => { Task.Delay(5000); return 123; } simply creates a new Task (which will complete in 5 seconds), then throws it away and immediately returns 123.

You can either:

  1. Block the calling thread, by doing Task.Delay(5000).Wait() (which does the same thing as Thread.Sleep(5000))
  2. Asynchronously wait for the Task returned from Task.Delay to complete: Task.Run(async () => { await Task.Delay(5000); return 123; })

Why does TaskTResult wait for result before awaiting it?

rom what I've read, I thought that assigning a task to a variable starts it asynchronously.

That is completely wrong.

Here, I've got a task: make a sandwich. I write that down on a piece of paper and I put it in a drawer marked "tasks". Did I start making a sandwich? No.

I start making a sandwich. Did starting to make a sandwich cause a piece of paper with a description of the task to appear in my drawer? No.

Variables and tasks have absolutely nothing to do with each other. Whatever caused you to believe that they had something to do with each other, stop believing that now.

When you need the return value, you just await it

Yes.

which ensures that the current thread waits for the task to complete.

No, if by "waits" you mean "blocks".

My understanding: the first statement should start the task

Sure, assuming that TaskOfTMethodAsync starts the task and returns the started task, which seems reasonable.

immediately after that the text box is appended

Yes.

Then the thread is blocked until integerTask is completed.

ABSOLUTELY NOT. The whole point of the await is to NOT block the thread! If you give me a task -- make a sandwich -- and you await me completing that task, you don't go have a nap while I'm making you a sandwich! You keep getting work done! Otherwise what is the point of hiring me to make you a sandwich? You want to hire workers to do work for you while you do something else, not while you sleep waiting for them to finish.

What am I doing wrong?

Two things. First, you're making a void method into a task, which is very wrong. The asynchronous method should not be void, it should return a task! A task represents asynchronous work. Second, you're only returning completed tasks!

Let's go through this line by line and say what it does.

var task = new Task(RunItAsync);

Create a task to represent the action.

task.Start();

Start the task on the thread pool.

The main thread then blocks on the completion of the task.

All right, this method is running on the thread pool:

static async void RunItAsync() 
{

What does this call do?

    var task = GetIntAsync();

Well, let's look:

static async Task<int> GetIntAsync()
{
return await Task.FromResult(GetIntSync());
}

First thing it does is call GetIntSync. That runs for a long time. Then it returns a value. Then we produce a completed task that represents that value. Then we await that task. Awaiting a completed task produces the value. So this is exactly the same as

   value1 = GetIntSync()
task1 = completed task representing value1
value2 = value of task1
task2 = completed task representing value2
return task2

There is nothing asynchronous here at all. This is a very, very complicated way to write "return GetIntSync()" -- you run the method, and then you construct not one but TWO completed tasks representing the task already completed!

Again: this method returns a task. All the sub-tasks of this method have completed already, so we simply return a completed task with the value.

What happens to that task? Remember we were at:

var task = GetIntAsync();

So that task is completed. (This is the "task2".)

Console.WriteLine("I'm writing something while the task is running...");

No, you're writing something after the task has completed. You returned a completed task!

// Should wait for the running task to complete and then output the result
Console.WriteLine(await task);

No, the task has already completed. This extracts the value without awaiting and prints it. There's nothing to await; the task is already done!

Now what is returned from this method? Nothing. Its a void method. So we now return, having done all the work of the method.

What happens next? The delegate passed to the original task has finished running on the worker thread, so the task running on the worker thread completes and signals the main thread that it can stop waiting.

Task.Result/wait(..) is indefinitely waits if waited on chain of tasks have 'unwrapped' task, whereas successfully completes if 'async/await' is used

Okay, let's try to get to the bottom of what's happening here.

First things first: the difference in the lambda passed to your ContinueWith is insignificant: functionally that part is identical in the two examples (at least as far as I can see).

Here's the FooAsync implementation which I used for testing:

static Task FooAsync()
{
return Task.Delay(500);
}

What I found curious is that using this implementation your IndefinitelyBlockingTask took twice as long as TaskWhichWorks (1 second vs ~500 ms respectively). Obviously the behaviour has changed due to Unwrap.

Someone with a keen eye would probably spot the problem right away, but personally I don't use task continuations or Unwrap that much, so it took a little while to sink in.

Here's the kicker: unless you use Unwrap on the continuation in both cases the task scheduled by ContinueWith completes synchronously (and immediately - regardless of how long the tasks created inside the loop take). The task created inside the lambda (Task.WhenAll(childTasks.ToArray()), let's call it inner task) is scheduled in a fire-and-forget fashion and runs onobserved.

Unwrapping the task returned from ContinueWith means that the inner task is no longer fire-and-forget - it is now part of the execution chain, and when you add it to the list, the outer task (Task.WhenAll(tasks.ToArray())) cannot complete until the inner task has completed).

Using ContinueWith(async () => { }) does not change the behaviour described above, because the task returned by the async lambda is not auto-unwrapped (think

// These two have similar behaviour and
// are interchangeable for our purposes.
Task.Run(() => Task.Delay(500))
Task.Run(async () => await Task.Delay(500));

vs

Task.Factory.StartNew(() => Task.Delay(500))

The Task.Run call has Unwrap built-in (see http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs#0fb2b4d9262599b9#references); the StartNew call doesn't and the task that it returns just completes immediately without waiting for the inner task. ContinueWith is similar to StartNew in that regard.

Side note

Another way to reproduce the behaviour observed when you use Unwrap is to make sure that tasks created inside the loop (or their continuations) are attached to the parent causing the parent task (created by ContinueWith) not to transition to the completed state until all child tasks have finished.

for (int i = 1; i <= 5; i++)
{
var ct = FooAsync().ContinueWith(_ => { }, TaskContinuationOptions.AttachedToParent);
childTasks.Add(ct);
}

Back to original problem

In your current implementation even if you had await Task.WhenAll(tasks.ToArray()) as the last line of the outer method, the method would still return before the tasks created inside the ContinueWith lambda have completed. Even if the tasks created inside ContinueWith never complete (my guess is that's exactly what's happening in your production code), the outer method will still return just fine.

So there it is, all things unexpected with the above code are caused by the silly ContinueWith which essentially "falls through" unless you use Unwrap. async/await is in no way the cause or the cure (although, admittedly, it can and probably should be used to rewrite your method in a more sensible way - continuations are difficult to work with leading to problems such as this one).

So what's happening in production

All of the above leads me to believe that there is a deadlock inside one of the tasks spun up inside your ContinueWith lambda causing that innner Task.WhenAll to never complete in production trim.

Unfortunately you have not posted a concise repro of the problem (I suppose I could do it for you armed with the above information, but it's really not my job to do so) or even the production code, so this is as much of a solution as I can give.

The fact that you weren't observing the described behaviour with your pseudo code should have hinted that you probably ended up stripping out the bit which was causing the problem. If you think that sounds silly, it's because it is, which is why I ended up retracting my original upvote for the question despite the fact that it is was the single most curious async problem I came across in a while.

CONCLUSION: Look at your ContinueWith lambda.

Final edit

You insist that Unwrap and await do similar things, which is true (not really as it ultimately messes with task composition, but kind of true - at least for the purpose of this example). However, having said that, you never fully recreated the Unwrap semantics using await, so is there really a big surprise that the method behaves differently? Here's TaskWhichWorks with an await that will behave similarly to the Unwrap example (it is also vulnerable to the deadlocking issues when applied to your production code):

static async Task TaskWhichUsedToWorkButNotAnymore()
{
List<Task> tasks = new List<Task>();
Task task = FooAsync();
tasks.Add(task);
Task<Task> continuationTask = task.ContinueWith(async t =>
{
List<Task> childTasks = new List<Task>();
for (int i = 1; i <= 5; i++)
{
var ct = FooAsync();
childTasks.Add(ct);
}
Task wa = Task.WhenAll(childTasks.ToArray());
await wa.ConfigureAwait(continueOnCapturedContext: false);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
tasks.Add(continuationTask);

// Let's Unwrap the async/await way.
// Pay attention to the return type.
// The resulting task represents the
// completion of the task started inside
// (and returned by) the ContinueWith delegate.
// Without this you have no reference, and no
// way of waiting for, the inner task.
Task unwrappedTask = await continuationTask;

// Boom! This method now has the
// same behaviour as the other one.
tasks.Add(unwrappedTask);

await Task.WhenAll(tasks.ToArray());

// Another way of "unwrapping" the
// continuation just to drive the point home.
// This will complete immediately as the
// continuation task as well as the task
// started inside, and returned by the continuation
// task, have both completed at this point.
await await continuationTask;
}

await vs Task.Wait - Deadlock?

Wait and await - while similar conceptually - are actually completely different.

Wait will synchronously block until the task completes. So the current thread is literally blocked waiting for the task to complete. As a general rule, you should use "async all the way down"; that is, don't block on async code. On my blog, I go into the details of how blocking in asynchronous code causes deadlock.

await will asynchronously wait until the task completes. This means the current method is "paused" (its state is captured) and the method returns an incomplete task to its caller. Later, when the await expression completes, the remainder of the method is scheduled as a continuation.

You also mentioned a "cooperative block", by which I assume you mean a task that you're Waiting on may execute on the waiting thread. There are situations where this can happen, but it's an optimization. There are many situations where it can't happen, like if the task is for another scheduler, or if it's already started or if it's a non-code task (such as in your code example: Wait cannot execute the Delay task inline because there's no code for it).

You may find my async / await intro helpful.

Doing something while waiting for a Task to complete

A important point is that everything after await will need to run on the same 'context' as before await. In practice this means that return age will need to run on the main thread. For this to work the main thread need to be free, i.e. it needs to process windows messages and not be blocked by user code.

Running while (!task.IsCompleted) will never free the main thread to complete the task, i.e. a classic deadlock. This is also a frequent problem when using Task.Result

To solve the problem, just remove the While-loop. If you want to provide feedback you could start a timer to do the printing, and stop it after the task has completed. Another alternative would insert a await Task.Delay(1000) in the loop-body, this should give the main thread a chance to do other things. I would not recommend inserting a delay for production code however. In any real situation a progress-bar would probably be the appropriate solution, but that is a bit out of scope for the question.

Is there a way to wait for all tasks until a specific result is true, and then cancel the rest?

Here's a solution based on continuation tasks. The idea is to append continuation tasks to each of the original (provided) tasks, and check the result there. If it's a match, the completion source will be set with a result (if there's no match, the result won't be set at all).

Then, the code will wait for whatever happens first: either all the continuation tasks complete, or the task completion result will be set. Either way, we'll be ready to check the result of the task associated with task completion source (that's why we wait for the continuation tasks to complete, not the original tasks) and if it's set, it's pretty much an indication that we have a match (the additional check at the end is a little paranoid, but better safe than sorry I guess... :D)

public static async Task<bool> WhenAnyHasResult<T>(Predicate<T> isExpectedResult, params Task<T>[] tasks)
{
const TaskContinuationOptions continuationTaskFlags = TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent;

// Prepare TaskCompletionSource to be set only when one of the provided tasks
// completes with expected result
var tcs = new TaskCompletionSource<T>();

// For every provided task, attach a continuation task that fires
// once the original task was completed
var taskContinuations = tasks.Select(task =>
{
return task.ContinueWith(x =>
{
var taskResult = x.Result;
if (isExpectedResult(taskResult))
{
tcs.SetResult(taskResult);
}
},
continuationTaskFlags);
});

// We either wait for all the continuation tasks to be completed
// (it's most likely an indication that none of the provided tasks completed with the expected result)
// or for the TCS task to complete (which means a failure)
await Task.WhenAny(Task.WhenAll(taskContinuations), tcs.Task);

// If the task from TCS has run to completion, it means the result has been set from
// the continuation task attached to one of the tasks provided in the arguments
var completionTask = tcs.Task;
if (completionTask.IsCompleted)
{
// We will check once more to make sure the result is set as expected
// and return this as our outcome
var tcsResult = completionTask.Result;
return isExpectedResult(tcsResult);
}

// TCS result was never set, which means we did not find a task matching the expected result.
tcs.SetCanceled();
return false;
}

Now, the usage will be as follows:

static async Task ExampleWithBooleans()
{
Console.WriteLine("Example with booleans");

var task1 = SampleTask(3000, true);
var task2 = SampleTask(5000, false);

var finalResult = await TaskUtils.WhenAnyHasResult(result => result == true, task1, task2);

// go ahead and cancel your cancellation token here

Console.WriteLine("Final result: " + finalResult);
Debug.Assert(finalResult == true);
Console.WriteLine();
}

What's nice about putting it into a generic method, is that it works with any type, not only booleans, as a result of the original task.



Related Topics



Leave a reply



Submit