如果所有“异步任务方法()”调用都返回 Task.FromResult() - 它会同步执行吗?

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

提问人:David Thielen 提问时间:11/18/2023 更新时间:11/18/2023 访问量:48

问:

我在为我的 Blazor Server 应用编写 bUnit 测试的上下文中问这个问题。

bUnit 的一个大问题是,在断言呈现页面中的内容之前,您需要完成渲染。

我调用了很多异步服务。对于单元测试,我有模拟服务,并且服务方法返回静态数据。async Task OnInitializedAsync()Task.FromResult()

在这种情况下,在方法中,我有:

_organization = await query.FirstOrDefaultAsync();

它是否立即建立了任务和回报?或者它是否看到任务已完成,分配值并继续执行?

换句话说,对于没有真正的异步活动的测试用例,它是否同步执行并返回已完成的?OnInitializedAsync()Task

ASP.net-core 异步-await blazor 服务器端 bunit

评论

1赞 Joshua 11/18/2023
若要自行回答此问题,请收集 Task 并立即调用 Task.IsCompleted,而无需等待它。
0赞 David Thielen 11/18/2023
@Joshua 不幸的是,这一切都是从 Blazor 调用的,所以我不能这样做。我可以验证它是否适用于我所做的简单测试,但我问是否发生的情况有细微差别。谢谢
0赞 Joshua 11/18/2023
顺便说一句,如果它适用于简单的测试,它适用于整个应用程序。它要么生成一个确定的同步答案,要么通过调度一个在执行时立即完成的任务来生成一个延迟的同步答案。
0赞 David Thielen 11/18/2023
@Joshua 99% 的时间我同意你的看法。但这些年来,我偶尔会遇到一些奇怪的、很少击中的例外情况,这些例外情况会在最糟糕的时候咬你的屁股。我遵循安迪·格罗夫(Andy Grove)的口头禅——只有偏执狂才能生存。

答:

2赞 Guru Stron 11/18/2023 #1

如果任务不是“真正”异步的或已完成,则等待后的代码将同步执行。简而言之 - 在“链接”的情况下,首先用于“真正异步”方法(即实际产生控件的方法)之前的所有内容都将同步执行:awaitawait

// 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");
}

您将看到所有 3 个(在本例中模拟同步/CPU 绑定工作,如果需要,您可以额外撒一些)语句将在“任务已启动...”一个,但其他一切都将在之后执行(演示@sharplab.io):Console.WriteLine(number)Thread.Sleep

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

现在,如果我们将“真正的异步”与实际上不异步的切换(例如):await Task.Delay(100);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");
}

输出将发生重大变化(demo @sharplab.io):

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

正如你所看到的,所有用相应的写入语句模拟的根“工作”都是在异步调用链之后完成的(我们甚至可以删除这不会改变结果)。await task;

完整性的原始代码:

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");
});

它给出以下输出:

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

评论

0赞 David Thielen 11/18/2023
I'm sorry, I'm lost. Where/when/how are each of these being called? What is factory? And for the output, I assume the "before/after await" are paired with the 2nd then 3rd ? And more importantly, is there any exception to this or by definition will this always be synchronous (the no Task.Yield() case)? TIATestTask
1赞 Joshua 11/18/2023
It's an accurate answer but it's nearly beyond my comprehension and I know how async works. OP doesn't. I don't see how he's expected to understand it.
0赞 Guru Stron 11/18/2023
@DavidThielen 1) Where/when/how are each of these being called - are the calls, at the start is just a local function 2) What is factory - just a function returning a (i.e. task is created and started when the factory function is invoked - ) 3) yes, when awaiting a completed or not "truly async" (i.e. one which does not yield control to the caller) function then it will be executed synchronously.await TestTaskasync Task TestTask(Func<Task> factory)Taskfactory()
0赞 Guru Stron 11/18/2023
@DavidThielen Updated answer a bit, hope it will make it more clear.
0赞 Guru Stron 11/20/2023
@Joshua Thank you, I see your point. Though I used this or similar code to explain the concept several times (and as far as I can tell was successful =). Tried to simplify it a bit.
0赞 Link 11/18/2023 #2

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 at least to a very small degree.async

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 gets called. Once that 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 is completed (the one with ) - therefore we continue "like a synchronous function".awaitawaitTaskTask.Delay(1)

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" for example. If 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.OnInitializedAsyncOnInitializedAsync

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 and friends, everything behaves synchronously.OnInitializedAsync

1赞 Theodor Zoulias 11/21/2023 #3

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

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

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

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

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

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