If all "async Task Method()" calls return Task.FromResult() - does that execute synchronously?

180 Views Asked by At

If all async Task Method() calls return Task.FromResult() - does that execute synchronously?

I am asking this in the context of writing bUnit tests for my Blazor server app.

A big issue for bUnit is you need the rendering to complete before asserting content in the rendered page.

My async Task OnInitializedAsync() call a lot of async services. For the unit tests I have mock services and ever service method returns a Task.FromResult() of static data.

In this case, when inside the method I have:

_organization = await query.FirstOrDefaultAsync();

Does it build up a task an return immediately? Or does it see that the task is completed, assign the value, and continue executing?

In other words, for the test case, where there is no true async activity, does it execute synchronously and OnInitializedAsync() returns a Task that is completed?

3

There are 3 best solutions below

0
On BEST ANSWER

I will expand a little with my answer before I talk about bUnit and will also go into simple examples of what @guru-stron wrote. For beginners, we have to explore the state machine of async at least to a very small degree.

Scenario 1

That is in @guru-stron example the first one.

var myTask = MyTask();
Console.WriteLine("In between Something");
await myTask;

async Task MyTask()
{
   await InnerMyTask();
   Console.WriteLine("In MyTask");
}

async Task InnerMyTask()
{
   await Task.Delay(1); // Or any async operation like going to a DB
   Console.WriteLine("In InnerMyTask");
}

This will result to:

In between Something
In InnerMyTask
In MyTask

In regards of this ominous state-machine. Think of "await" as a method to slice your method into smaller methods. For each "await" you have one method that has the content from the last await (or beginning) to the current await. Now if you await something and you spin up the Task - you give back control to the caller. You do this process for each caller in the chain that also uses "await". If you don't await (like in my given example the first calling function) then you keep the control flow inside that methods until await gets called. Once that await is hit your Task tries to continue (there is a lot more involved, but let's try to keep it simple). Now the most inner Task is completed (the one with Task.Delay(1)) - therefore we continue "like a synchronous function".

So as we don't directly await in the most outer function - we have "In between Something" and then the Console.WriteLine from the most inner and so on.

The Blazor Renderer, on which bUnits Renderer ultimately is based on, behaves like that. It is literally like the most outer function. So it "sees" OnInitializedAsync for example. If OnInitializedAsync goes to a db asynchronously then exactly that process I describes kicks in - therefore the renderer is "done" even though there will be future work.

Scneario 2

Now if we take the example above, but directly return a completed Task:

var myTask = MyTask();
Console.WriteLine("In between Something");
await myTask;

async Task MyTask()
{
   await InnerMyTask();
   Console.WriteLine("In MyTask");
}

async Task InnerMyTask()
{
   await Task.CompletedTask; // Mocked our DB call
   Console.WriteLine("In InnerMyTask");
}

we get this:

In InnerMyTask
In MyTask
In between Something

I hope now that makes sense, as we never "give back" control to the caller! There "is nothing to await" (simplified).

So if you have completed Tasks inside OnInitializedAsync and friends, everything behaves synchronously.

0
On

If all async Task Method() calls return Task.FromResult() - does that execute synchronously?

Yes. The continuation after the await of a completed task runs synchronously, on the same thread as the code before the await.

await Task.FromResult(0); // Continues synchronously

This happens for any number of awaits. The for loop below runs synchronously as well.

// All this runs synchronously
for (int i = 0; i < 1000; i++)
{
    await Task.FromResult(i);
}

The .NET async state machine uses a scheduler to schedule the continuation asynchronously only when it finds an awaitable with the property TaskAwaiter.IsCompleted having the value false. Otherwise it just runs the continuation synchronously.

5
On

If task is not "truly" async or is completed then code after awaiting it will be executed synchronously. In short - in case of "chained" await's everything before first await for "truly async" method (i.e. one which actually yields the control) will be executed synchronously:

// this will start a task but will not block to wait for delay
var task = First();
Console.WriteLine("Task started but not blocked");
await task;
Console.WriteLine("After root await");

async Task First()
{
    Console.WriteLine("First");
    await Second();
    Console.WriteLine("After First");
}

async Task Second()
{
    Console.WriteLine("Second");
    await Third();
    Console.WriteLine("After Second");
}

async Task Third()
{
    Console.WriteLine("Third");
    await Task.Delay(100);
    Console.WriteLine("After Third");
}

And you will see that all 3 Console.WriteLine(number) (which simulate sync/CPU-bound work in this case, you can sprinkle some Thread.Sleep's if you want in addition) statements will be executed before the "Task started..." one but everything else will be executed after (demo @sharplab.io):

First
Second
Third
Task started but not blocked
After Third
After Second
After First
After root await

Now if we switch the "truly async" await Task.Delay(100); with a not actually async one (for example Task.CompletedTask):

async Task Third()
{
    Console.WriteLine("Third");
    await Task.CompletedTask; // not actually async, does not return control to the caller
    Thread.Sleep(100);
    Console.WriteLine("After Second");
}

The output will change significantly (demo @sharplab.io):

First
Second
Third
After Third
After Second
After First
Task started but not blocked
After await

As you see all the root "work" simulated with corresponding write statements was completed after the async call chain (we can even remove await task; this will not change the result).

Original code for completeness:

async Task TestTask(Func<Task> factory)
{
    Console.WriteLine("Before task");
    var t = factory();
    Console.WriteLine("Task created");
    await t;
    Console.WriteLine("After awaiting task");
}

// not actually async:
await TestTask(async () =>
{
    Console.WriteLine("    before await");
    await Task.CompletedTask;
    Console.WriteLine("    after await");
});

Console.WriteLine("---------------");

// truly async
await TestTask(async () =>
{
    Console.WriteLine("    before await");
    await Task.Yield();
    Console.WriteLine("   after await");
});

Which gives the following output:

Before task
    before await
    after await
Task created
After awaiting task
---------------
Before task
    before await
Task created
   after await
After awaiting task