C#中“返回等待”的目的是啥?

Posted

技术标签:

【中文标题】C#中“返回等待”的目的是啥?【英文标题】:What is the purpose of "return await" in C#?C#中“返回等待”的目的是什么? 【发布时间】:2013-10-06 13:08:57 【问题描述】:

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

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 的问题确实有返回语句吗? 【参考方案1】:

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

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

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

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

更新:我写了一个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'".【参考方案2】:

您想要这样做的唯一原因是,如果之前的代码中还有其他 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,但我描述的情况是使用它可以增加价值。【参考方案3】:

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 异步代码库时发生冲突,因此对我来说无法使用。【参考方案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】:

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

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

【讨论】:

【参考方案6】:

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

当您可以从内部 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;【参考方案7】:

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

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

例如:

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

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

【讨论】:

这是一篇很好的文章:vkontech.com/…

以上是关于C#中“返回等待”的目的是啥?的主要内容,如果未能解决你的问题,请参考以下文章

“受保护的内部”范围的目的是啥[重复]

System.Data.SqlClient.SqlParameter.IsNullable 的目的是啥?

获取仅包含文本条目的组合框的选定文本的最简单方法是啥?

Docker,它是啥,目的是啥

PHP中@的目的是啥[重复]

在“回流”中,行动的目的是啥?