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


251

没有这样的编写方法的场景:

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();
}

会有意义吗?

return await当您可以直接Task<T>从内部DoAnotherThingAsync()调用返回时,为什么要使用struct?

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


2
我认为您看到此消息的唯一原因是因为人们通过模仿来学习,并且通常(如果不需要)他们使用可以找到的最简单的解决方案。因此,人们看到了该代码,使用了该代码,他们看到了它的工作,从现在开始,对于他们来说,这是正确的方法...在这种情况下等待没有用
Fabio Marcolini

7
至少有一个重要的区别:异常传播
noseratio 2014年

1
我也不理解,根本无法理解整个概念,也没有任何意义。从我了解的方法是否具有返回类型的角度来看,它必须具有return关键字,这不是C#语言的规则吗?
怪物

@monstro OP的问题有返回语句吗?
David Klempfner '19

Answers:


189

有一种偷偷摸摸的情况,当return普通方法和return awaitin async方法的行为不同时:与using(或更一般地,return await在一个try块中)组合时。

考虑方法的以下两个版本:

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

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

第一种方法将Dispose()所述Foo对象,一旦DoAnotherThingAsync()方法返回,这很可能是长期实际完成之前。这意味着第一个版本可能存在错误(因为Foo处理过早),而第二个版本则可以正常工作。


4
为了完整foo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
起见

7
@ghord那行不通,Dispose()返回void。您将需要类似的东西return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });。但是我不知道当您可以使用第二个选项时为什么要这么做。
svick 2014年

1
@svick您是正确的,应该更像{ var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }。用例非常简单:如果您使用的是.NET 4.0(与大多数其他应用程序一样),您仍然可以通过这种方式编写异步代码,该代码可以很好地在4.5应用程序中调用。
ghord 2014年

2
@ghord如果您在.Net 4.0上并且要编写异步代码,则可能应该使用Microsoft.Bcl.Async。而且您的代码Foo仅在返回Task完成后才处理,而我不希望这样做,因为它不必要地引入了并发性。
2014年

1
@svick您的代码也等待任务完成。另外,由于对KB2468871的依赖性,Microsoft.Bcl.Async对我不可用,并且在使用带有适当4.5异步代码的.NET 4.0异步代码库时发生冲突。
ghord 2014年

93

如果不需要async(例如,您可以Task直接退回),请不要使用async

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

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

有关async性能的更多信息,请参见Stephen Toub的MSDN文章和有关该主题的视频

更新:我写了一篇博客文章,其中有更多细节。


13
您能否添加一个解释,说明为什么await在第二种情况下有用?为什么不return SecondAwait(intermediate);呢?
马特·史密斯

2
我和马特有同样的问题,return SecondAwait(intermediate);在那种情况下也不会实现目标吗?我认为return await这里也是多余的...
TX_

23
@MattSmith无法编译。如果要await在第一行中使用,则也必须在第二行中使用它。
2013年

2
@cateyes我不确定“并行化它们带来的开销”是什么意思,但是该async版本将比同步版本使用更少的资源(线程)。
svick 2014年

4
@TomLint 确实无法编译。假设返回类型SecondAwait为'string,错误消息为:“ CS4016:由于这是一个异步方法,因此返回表达式必须为'string'类型,而不是'Task <string>'。
svick '16

23

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


4
与斯蒂芬的答案一样,即使先前的代码中还有其他等待,我也不明白为什么return await有必要这样做(而不是仅仅返回子调用的任务)。您能提供解释吗?
TX_

10
@TX_如果您要删除,async那么您将如何等待第一个任务?您需要将方法标记为async好像要使用任何等待方式。如果该方法标记为,async并且您的await代码较早,则需要进行await第二次异步操作才能使其具有正确的类型。如果您只是删除了await它,那么它将不会编译,因为返回值将不是正确的类型。由于方法是async结果,因此总是将其包装在任务中。
Servy 2013年

11
@Noseratio尝试两个。第一次编译。第二个没有。错误消息将告诉您问题所在。您将不会返回正确的类型。当在async方法中不返回任务时,将返回任务的结果,然后将其包装。
Servy 2013年

3
@Servy,当然-你是对的。在后一种情况下,我们将Task<Type>明确返回,而async指示返回Type(编译器本身将变为Task<Type>)。
noseratio 2013年

3
@Itsik可以肯定,async这只是用于显式连接延续的语法糖。您无需 async执行任何操作,但是在执行任何非平凡的异步操作时,使用起来非常容易。例如,您提供的代码实际上并没有像您希望的那样传播错误,而在更复杂的情况下正确地执行操作变得越来越困难。尽管您永远不需要 async,但我描述的情况是使用它的价值所在。
Servy '16

17

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

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

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

在这种情况下,GetIFooAsync()必须等待的结果,GetFooAsync因为T这两种方法之间的类型不同,并且Task<Foo>不能直接分配给Task<IFoo>。但是,如果你等待的结果,它只是成为Foo直接分配给IFoo。然后,异步方法只是将结果重新打包到内部,Task<IFoo>然后您就可以使用了。


1
同意,这确实很烦人-我相信根本原因Task<>是不变的。
StuartLC '19

7

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

所以是的,当性能不是很关键(通常不是)时,我将对所有这些重击方法进行异步处理,以使我拥有异步状态机来帮助我以后诊断挂起,并帮助确保是否重击方法随着时间的推移而不断发展,他们将确保返回错误的任务,而不是抛出错误。


6

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

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

例如:

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

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


2

这也使我感到困惑,我认为以前的答案忽略了您的实际问题:

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

好吧,有时候您实际上想要一个Task<SomeType>,但是大多数时候您实际上想要一个SomeType,即任务的结果。

从您的代码:

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

一个不熟悉语法的人(例如,我)可能会认为此方法应该返回a Task<SomeResult>,但是由于用标记async,因此它的实际返回类型为SomeResult。如果仅使用return foo.DoAnotherThingAsync(),则将返回无法编译的Task。正确的方法是返回任务的结果,因此return await


1
“实际返回类型”。嗯 异步/等待不会更改返回类型。在您的示例中var task = DoSomethingAsync();,您可以执行任务,而不是T
Shoe

2
@Shoe我不确定我是否了解这async/await件事。据我了解,,Task task = DoSomethingAsync()虽然Something something = await DoSomethingAsync()两者都能正常工作。第一个任务为您提供适当的任务,而第二个任务为await关键字提供了任务完成后的结果。例如,我可以拥有Task task = DoSomethingAsync(); Something something = await task;
heltonbiker
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.