提问人:David Thielen 提问时间:11/18/2023 更新时间:11/18/2023 访问量:48
如果所有“异步任务方法()”调用都返回 Task.FromResult() - 它会同步执行吗?
If all "async Task Method()" calls return Task.FromResult() - does that execute synchronously?
问:
我在为我的 Blazor Server 应用编写 bUnit 测试的上下文中问这个问题。
bUnit 的一个大问题是,在断言呈现页面中的内容之前,您需要完成渲染。
我调用了很多异步服务。对于单元测试,我有模拟服务,并且服务方法返回静态数据。async Task OnInitializedAsync()
Task.FromResult()
在这种情况下,在方法中,我有:
_organization = await query.FirstOrDefaultAsync();
它是否立即建立了任务和回报?或者它是否看到任务已完成,分配值并继续执行?
换句话说,对于没有真正的异步活动的测试用例,它是否同步执行并返回已完成的?OnInitializedAsync()
Task
答:
如果任务不是“真正”异步的或已完成,则等待后的代码将同步执行。简而言之 - 在“链接”的情况下,首先用于“真正异步”方法(即实际产生控件的方法)之前的所有内容都将同步执行:await
await
// 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
评论
TestTask
await TestTask
async Task TestTask(Func<Task> factory)
Task
factory()
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".await
await
Task
Task.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.OnInitializedAsync
OnInitializedAsync
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
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 .await
await
await Task.FromResult(0); // Continues synchronously
This happens for any number of s. The loop below runs synchronously as well.await
for
// 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.async
false
评论