【问题标题】:What is the purpose of "return await" in C#?C#中“返回等待”的目的是什么?
【发布时间】:2020-10-17 03:11:33
【问题描述】:

是否有任何这样的写法场景:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

而不是这个:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

有意义吗?

既然可以从内部 DoAnotherThingAsync() 调用中直接返回 Task&lt;T&gt;,为什么还要使用 return await 构造?

我在很多地方都看到了带有return await 的代码,我想我可能遗漏了一些东西。但据我了解,在这种情况下不使用 async/await 关键字并直接返回 Task 在功能上是等效的。为什么要增加额外的await 层的额外开销?

【问题讨论】:

  • 我认为你看到这个的唯一原因是因为人们通过模仿学习,并且通常(如果他们不需要)他们使用他们能找到的最简单的解决方案。所以人们看到那个代码,使用那个代码,他们看到它有效,从现在开始,对他们来说,这是正确的方法......在这种情况下等待是没有用的
  • 至少有一个重要区别:exception propagation
  • 我也看不懂,完全理解不了这个概念,没有任何意义。从我了解到如果一个方法有一个返回类型,它必须有一个返回关键字,这不是 C# 语言的规则吗?
  • @monstro OP 的问题确实有返回语句吗?

标签: c# .net .net-4.5 async-await


【解决方案1】:

return 在普通方法中和return awaitasync 方法中的行为不同时,有一种偷偷摸摸的情况:当与using 结合使用时(或者,更一般地,在try 块中的任何return await) .

考虑这两个版本的方法:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

一旦DoAnotherThingAsync() 方法返回,第一个方法将Dispose() Foo 对象,这可能在它实际完成之前很久。这意味着第一个版本可能有问题(因为Foo 处理得太早了),而第二个版本可以正常工作。

【讨论】:

  • 为了完整起见,在第一种情况下,您应该返回foo.DoAnotherThingAsync().ContinueWith(_ =&gt; foo.Dispose());
  • @ghord 那行不通,Dispose() 返回void。你需要像return foo.DoAnotherThingAsync().ContinueWith(t -&gt; { foo.Dispose(); return t.Result; }); 这样的东西。但我不知道当你可以使用第二个选项时,你为什么要这样做。
  • @svick 你是对的,它应该更像{ var task = DoAnotherThingAsync(); task.ContinueWith(_ =&gt; foo.Dispose()); return task; }。用例非常简单:如果您使用 .NET 4.0(像大多数人一样),您仍然可以以这种方式编写异步代码,这将很好地从 4.5 应用程序调用。
  • @ghord 如果您使用 .Net 4.0 并且想要编写异步代码,您可能应该使用Microsoft.Bcl.Async。只有在返回的 Task 完成后,您的代码才会处理 Foo,我不喜欢,因为它不必要地引入了并发性。
  • @svick 您的代码也会等到任务完成。此外,Microsoft.Bcl.Async 由于对 KB2468871 的依赖以及在使用具有正确 4.5 异步代码的 .NET 4.0 异步代码库时发生冲突,因此对我来说无法使用。
【解决方案2】:

如果您不需要async(即您可以直接返回Task),则不要使用async

在某些情况下return await 很有用,比如您有两个异步操作要做:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

有关 async 性能的更多信息,请参阅 Stephen Toub 的 MSDN articlevideo 主题。

更新:我写了一个blog post,其中包含更多细节。

【讨论】:

  • 您能否解释一下为什么await 在第二种情况下有用?为什么不return SecondAwait(intermediate);
  • 我和马特有同样的问题,return SecondAwait(intermediate); 在这种情况下不会也达到目标吗?我认为return await 在这里也是多余的......
  • @MattSmith 那不会编译。如果你想在第一行使用await,你也必须在第二行使用它。
  • @svick 因为它们只是按顺序运行,所以应该将它们更改为像 var intermediate = First(); return Second(intermediate) 这样的普通调用以避免并行引入的开销。在这种情况下不需要异步调用,是吗?
  • @TomLint It really doesn't compile. 假设SecondAwait的返回类型是`string,错误信息是:"CS4016: 由于这是一个异步方法,返回表达式必须是'string'类型而不是 'Task'".
【解决方案3】:

您想要这样做的唯一原因是,如果之前的代码中还有其他 await,或者您在返回结果之前以某种方式对结果进行了操作。可能发生这种情况的另一种方式是通过try/catch 更改异常处理方式。如果您没有这样做,那么您是对的,没有理由增加创建方法 async 的开销。

【讨论】:

  • 与斯蒂芬的回答一样,我不明白为什么return await 是必要的(而不是仅仅返回子调用的任务)即使在早期代码中还有其他等待。你能解释一下吗?
  • @TX_ 如果您想删除async,那么您将如何等待第一个任务?如果要使用 any 等待,则需要将该方法标记为 async。如果该方法被标记为async 并且您在代码的前面有一个await,那么您需要await 第二个异步操作才能使其具有正确的类型。如果您刚刚删除了await,那么它将无法编译,因为返回值的类型不正确。由于方法是async,结果总是包装在一个任务中。
  • @Noseratio 试试这两个。第一个编译。第二个没有。错误消息会告诉您问题所在。您不会返回正确的类型。当在 async 方法中你不返回任务时,你返回任务的结果,然后将被包装。
  • @Servy,当然 - 你是对的。在后一种情况下,我们将显式返回Task&lt;Type&gt;,而async 要求返回Type(编译器本身会变成Task&lt;Type&gt;)。
  • @Itsik 当然,async 只是用于显式连接延续的语法糖。您不需要需要 async 做任何事情,但是在执行任何非平凡的异步操作时,它显着地 更容易使用。例如,您提供的代码实际上并没有像您希望的那样传播错误,并且在更复杂的情况下正确执行此操作开始变得相当困难。虽然您永远需要 async,但我描述的情况是使用它可以增加价值。
【解决方案4】:

您可能需要等待结果的另一种情况是:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

在这种情况下,GetIFooAsync() 必须等待GetFooAsync 的结果,因为T 的类型在两种方法之间是不同的,并且Task&lt;Foo&gt; 不能直接分配给Task&lt;IFoo&gt;。但是,如果您等待结果,它就会变成Foo 可以直接分配给IFoo。然后 async 方法只是将结果重新打包到 Task&lt;IFoo&gt; 中,然后就可以走了。

【讨论】:

  • 同意,这真的很烦人——我相信根本原因是Task&lt;&gt; 是不变的。
【解决方案5】:

如果您不使用 return await,则可能会在调试时或在异常日志中打印时破坏堆栈跟踪。

当您返回任务时,该方法实现了它的目的,并且它不在调用堆栈中。 当您使用 return await 时,您会将其留在调用堆栈中。

例如:

使用 await 时的调用堆栈: A 等待 B 的任务 => B 等待 C 的任务

使用 await 时调用堆栈: A 等待 C 的任务,B 已返回。

【讨论】:

【解决方案6】:

使原本简单的“thunk”方法异步在内存中创建一个异步状态机,而非异步则不会。虽然这通常可以指出人们使用非异步版本,因为它更有效(这是真的),但这也意味着在挂起的情况下,您没有证据表明该方法涉及“返回/继续堆栈”这有时会使人们更难以理解挂起。

所以是的,当性能不重要时(而且通常不是),我将在所有这些 thunk 方法上抛出异步,以便我有异步状态机来帮助我稍后诊断挂起,并帮助确保如果这些 thunk 方法随着时间的推移而发展,它们肯定会返回错误的任务而不是 throw。

【讨论】:

    【解决方案7】:

    这也让我感到困惑,我觉得以前的答案忽略了你的实际问题:

    当您可以从内部 DoAnotherThingAsync() 调用中直接返回 Task 时,为什么要使用 return await 构造?

    嗯,有时你实际上想要一个Task&lt;SomeType&gt;,但大多数时候你实际上想要一个SomeType的实例,即任务的结果。

    来自您的代码:

    async Task<SomeResult> DoSomethingAsync()
    {
        using (var foo = new Foo())
        {
            return await foo.DoAnotherThingAsync();
        }
    }
    

    不熟悉语法的人(比如我)可能会认为这个方法应该返回一个Task&lt;SomeResult&gt;,但是由于它被标记为async,这意味着它的实际返回类型是SomeResult。 如果您只使用return foo.DoAnotherThingAsync(),您将返回一个无法编译的任务。正确的做法是返回任务的结果,所以return await

    【讨论】:

    • “实际返回类型”。诶? async/await 不会改变返回类型。在您的示例中,var task = DoSomethingAsync(); 会给您一个任务,而不是 T
    • @Shoe 我不确定我是否理解async/await 的事情。据我了解,Task task = DoSomethingAsync()Something something = await DoSomethingAsync() 都有效。第一个为您提供正确的任务,而第二个,由于await 关键字,在任务完成后为您提供任务的结果。例如,我可以拥有Task task = DoSomethingAsync(); Something something = await task;
    猜你喜欢
    • 2017-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多