C# 返回已完成任务的异步方法异常缓慢

C# 返回已完成任务的异步方法异常缓慢,c#,.net,async-await,C#,.net,Async Await,我有一些在Web服务器上运行良好的c#代码。代码使用async/await,因为它在生产环境中执行一些网络调用 我还需要对代码进行一些模拟;在模拟过程中,代码被并发调用数十亿次。模拟不执行任何网络调用:使用模拟,它使用Task.FromResult()返回一个值。模拟返回的值实际上模拟了生产环境中可以接收到的来自网络调用的所有可能响应 我不理解使用async/await会有一些开销,但我也希望在返回已经完成的任务并且不应该有实际等待的情况下,性能不会有很大的差异 但在进行一些测试时,我注意到性能

我有一些在Web服务器上运行良好的c#代码。代码使用async/await,因为它在生产环境中执行一些网络调用

我还需要对代码进行一些模拟;在模拟过程中,代码被并发调用数十亿次。模拟不执行任何网络调用:使用模拟,它使用Task.FromResult()返回一个值。模拟返回的值实际上模拟了生产环境中可以接收到的来自网络调用的所有可能响应

我不理解使用async/await会有一些开销,但我也希望在返回已经完成的任务并且不应该有实际等待的情况下,性能不会有很大的差异

但在进行一些测试时,我注意到性能大幅下降(特别是在某些硬件上)

我在启用编译器优化的情况下使用LinqPad测试了以下代码;如果要在visual studio中直接测试,可以删除.Dump()调用并将代码粘贴到控制台应用程序中

//同步版本
void Main()
{
可枚举的范围(0,1\u 000\u 000)
.天冬酰胺()
.合计(
() => 0.0,
(a,i)=>计算(a,i),
(a1,a2)=>a1+a2,
f=>f
)
.Dump();
}
双计算(双a,双i)=>a+Math.Sin(i);

//异步等待版本
void Main()
{
可枚举的范围(0,1\u 000\u 000)
.天冬酰胺()
.合计(
() => 0.0,
(a,i)=>计算(a,i)。结果,
(a1,a2)=>a1+a2,
f=>f
)
.Dump();
}
异步任务计算(双a,双i)=>a+Math.Sin(i);
代码的异步等待版本举例说明了我的模拟代码的情况

我在我的i7机器上非常成功地运行了模拟。但当我试图在办公室的AMD ThreadRipper机器上运行代码时,我得到了一些非常糟糕的结果

我已经在我的i7机器和AMD ThreadRipper上使用上述linq pad中的代码运行了一些基准测试,结果如下:

TEST on i7 quad-core 3,67 Ghz (windows 10 pro x64):

sync version: 15 sec (100% CPU)
async-await version: 20 sec (93% CPU)
TEST on i7

"sync" version: 16 sec (100% CPU)
"await Task" version: 49 sec (95% CPU)
"await ValueTask" version: 31 sec (100% CPU)
我知道有硬件方面的差异(也许英特尔的超线程更好,等等),但这个问题与硬件性能无关

为什么不总是100%的CPU使用率(或者50%考虑到CPU超读的最坏情况),但异步等待版本的代码中CPU使用率有所下降?

(AMD的CPU使用率下降幅度更大,但英特尔也出现了这种情况)

是否存在不重构代码中所有异步等待调用链的变通方法?(代码库又大又复杂)

多谢各位

编辑

正如在一篇评论中所建议的,我尝试使用ValueTask而不是Task,它似乎解决了这个问题。我在VS中直接尝试了这一点,因为我需要一个nuget包(发布版本),结果如下:

TEST on i7 quad-core 3,67 Ghz (windows 10 pro x64):

sync version: 15 sec (100% CPU)
async-await version: 20 sec (93% CPU)
TEST on i7

"sync" version: 16 sec (100% CPU)
"await Task" version: 49 sec (95% CPU)
"await ValueTask" version: 31 sec (100% CPU)

老实说,我对ValueTask课程了解不多,我打算研究一下。如果你能在回答中解释/详细说明,那是欢迎的


谢谢。

您的垃圾收集器很可能配置为工作站模式(默认),该模式使用单个线程回收未使用对象分配的内存。对于一台32芯的机器,一个芯肯定不足以清理其余31芯不断产生的混乱!因此,您可能应该切换到:


后台服务器垃圾回收使用多个线程,通常为每个逻辑处理器使用一个专用线程


通过使用s而不是
Task
s,可以避免堆中的内存分配,因为
ValueTask
是在堆栈中分配的结构,不需要垃圾收集。但只有当它包装完成任务的结果时,情况才会如此。如果它包装了一个不完整的任务,那么它就没有优势。它适用于您必须等待数千万个任务,并且您希望绝大多数任务都能完成的情况。

我想解决以下问题:

代码的异步等待版本举例说明了我的生产代码的情况

您说过您的产品版本“执行一些网络调用”。如果是这样的话,那么您在这里展示的代码并不是您的产品代码的示例。Lasse在评论中提到了原因:您的
async
方法没有异步运行。原因在于
wait
的工作方式

wait
关键字查看您正在调用的方法返回的
任务。您知道它将暂停方法的执行,并将方法的其余部分注册为
任务的延续。但您可能不知道的是,只有当
任务尚未完成时才会发生这种情况。如果当
wait
查看
任务时,该任务已完成,则代码将同步进行。事实上,您应该看到编译器警告告诉您:

CS1998:此异步方法缺少'await'运算符,将同步运行。考虑使用“Acess”操作符等待非阻塞API调用,或“等待任务.Run(…)”在后台线程上执行CPU绑定的工作。 因此,两个代码块之间的唯一区别在于
async
版本只是增加了
wait
的不必要开销,使其仍然同步运行

要有一个真正的异步方法,实际上必须做一些需要等待的事情。如果要模拟此情况,可以使用。即使您使用尽可能最小的延迟(
Task.delay(TimeSpan.FromTicks(1))
),它仍然会触发
wait
来完成它的工作

async Task<double> Calc(double a, double i)
{
    await Task.Delay(TimeSpan.FromTicks(1));
    return a + Math.Sin(i);
}
在我的Intel Core i7上,第
double Calc(double a, double i)
{
    Thread.Sleep(TimeSpan.FromTicks(1));
    return a + Math.Sin(i);
}