Kazarp Kazarp - 2 months ago 8
C# Question

Why does Task<TResult> wait for result before awaiting it?

I'm trying to understand TPL. Unfortunately I can't get over Tasks with return types. From what I've read, I thought that assigning a task to a variable starts it asynchronously. When you need the return value, you just await it, which ensures that the current thread waits for the

Task<T>
to complete.

Example on MSDN:

// Call and await in separate statements.
Task<int> integerTask = TaskOfT_MethodAsync();

// You can do other work that does not rely on integerTask before awaiting.
textBox1.Text += String.Format("Application can continue working while the Task<T> runs. . . . \r\n");

int result2 = await integerTask;


My understanding: the first statement should start the task, immediately after that the text box is appended. Then the thread is blocked until
integerTask
is completed.

However, when I was trying it on my own, it didn't work that way:

static void Main()
{
var task = new Task(RunItAsync);
task.Start();
task.Wait();
}
static async void RunItAsync()
{
// Should start the task, but should not block
var task = GetIntAsync();
Console.WriteLine("I'm writing something while the task is running...");
// Should wait for the running task to complete and then output the result
Console.WriteLine(await task);
}
static Random r = new Random();
static async Task<int> GetIntAsync()
{
return await Task.FromResult(GetIntSync());
}
public static int GetIntSync()
{
// Some long operation to hold the task running
var count = 0;
for (var i = 0; i < 1000000000; i++) {
if (i % 2 == 0) count++;
}
return r.Next(count);
}


There is no output, after a few seconds it outputs everything at once:

I'm writing something while the task is running...
143831542


What am I doing wrong?

Answer

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.

Comments