过度使用async await?

3

我想知道如果我在代码中过度使用asyncawait,是否会存在(性能)惩罚?

我经常这样做:

static void Main()
{
    var result = Task<int>.Run (()=> S1Async(1)).Result;
    Console.WriteLine(result);
}

static async Task<int> WrapperAsync(Func<int, Task<int>> func) => await func(2);
static async Task<int> S1Async(int x) => await WrapperAsync(async t=> await S2Async(x * t));
static async Task<int> S2Async(int x) => await WrapperAsync(async t=> await S3Async(x * t));
static async Task<int> S3Async(int x) => await WrapperAsync(async t=> await S4Async(x * t));
static async Task<int> S4Async(int x) => await Task.FromResult(x * 10);

我认为可以跳过使用async-await,并且这段代码是类似的:

static void Main()
{
    var result = Task<int>.Run(() => S1Async(1)).Result;
    Console.WriteLine(result);
}

static Task<int> WrapperAsync(Func<int, Task<int>> func) => func(2);
static Task<int> S1Async(int x) => WrapperAsync(t => S2Async(x * t));
static Task<int> S2Async(int x) => WrapperAsync(t => S3Async(x * t));
static Task<int> S3Async(int x) => WrapperAsync(t => S4Async(x * t));
static Task<int> S4Async(int x) => Task.FromResult(x * 10);

当任务嵌套且每个级别只有一个任务时,是否可以安全地跳过async/await?

两个代码示例在LinqPad中给出相同的结果,因此我认为它们是相似的,但也许有一些副作用需要注意吗?


我认为这段代码最大的问题是混合使用async-await和阻塞操作(使用Result或者Wait)。如果使用不正确,这种做法可能会导致死锁。如果不使用async-await而是返回一个Task也是可以的,只要你不在try块中这样做,因为如果你没有等待(await),那么异常可能会在你离开try块之后抛出。 - juharr
什么是惩罚?你的问题是,如果执行变慢,那么是否要同步执行所有内容?5个线程互相等待,你知道自己在做什么……这没有任何问题。线程池会照顾好它,你不会同时启动太多线程(但阈值超过100)。你可以用2个线程陷入死锁,而且如果编程不小心,可能会在200个线程上增加这种情况的机会。 在单核CPU上,异步执行与同步执行相比,惩罚非常小。但是,单核CPU今天只存在于虚拟机中。 - Holger
在这里玩一下:https://sharplab.io/,你会看到两者之间的差异。实际差异的成本取决于您正在进行的计算,但如果“有用”代码和“开销”之间的关系不好,则可能相当大。 - ZorgoZ
1个回答

1

简短的回答是:在这种情况下,您可以跳过它们,但是会有性能损失。

当您的方法只执行一次await并立即返回时,就像这样:

async Task FooAsync()
{
    /* ... */

    await BarAsync();
}

那么它基本上相当于写:

Task FooAsync()
{
    /* ... */

    return BarAsync();
}
< p >< code >async 的意思是(从 < code >FooAsync 的角度来看):好的,操作 < code >BarAsync 可能需要一段时间才能完成,所以如果我们从中获得一个未完成的 < code >Task,让我保存当前状态并将控制流返回给我的调用者。一旦 < code >Task 完成,我就希望恢复状态并继续进行。

在这种情况下,明显没有额外的工作需要在 < code >BarAsync < code >Task 完成后执行,因此您可以将该 < code >Task 直接返回给调用者,因为实际上 < code >FooAsync 与 < code >Task 完成的时间完全相同。没有必要保存任何状态或安排延续。

当我们谈论单个调用时,这方面的开销并不大,但如果你多次调用这个方法,可能会感受到影响。编译器在你声明方法为async时必须设置整个async基础设施 - 状态机、继续调度等等。所以作为一般规则:如果你可以简化方法,使其不是async而只是返回另一个Task,那么值得这样做。

顺便说一下:自从C# 7.1以后,你肯定不需要将S1Async调用包装在Task.Run中,也不需要在Main中同步阻塞。你可以直接写:

static async Task Main()
{
    var result = await S1Async(1);
    Console.WriteLine(result);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接