在C#中"返回等待"的目的是什么?

TX_*_*TX_ 219 .net c# async-await .net-4.5

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

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

而不是这个:

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

会有意义吗?

为什么return await在可以直接Task<T>从内部DoAnotherThingAsync()调用返回时使用构造?

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

svi*_*ick 172

有当一个欺骗性的情况下return在正常的方法和return awaitasync方法不同的行为:当与组合using(或,更一般地,任何return awaittry块).

考虑这两个版本的方法:

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

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

方法返回时,第一个方法将Dispose()Foo对象DoAnotherThingAsync(),它可能在实际完成之前很久.这意味着第一个版本可能是错误的(因为Foo太快了),而第二个版本可以正常工作.

  • 为了完整起见,在第一种情况下,您应该返回 `foo.DoAnotherThingAsync().ContinueWith(_ =&gt; foo.Dispose());` (4认同)
  • @ghord那是行不通的,Dispose()返回void。您将需要类似`return foo.DoAnotherThingAsync()。ContinueWith(t-&gt; {foo.Dispose(); return t.Result;});`之类的东西。但我不知道当您可以使用第二个选项时为什么要这么做。 (4认同)
  • @ghord如果您使用的是.Net 4.0,并且想要编写异步代码,则可能应该使用[Microsoft.Bcl.Async](http://www.nuget.org/packages/Microsoft.Bcl.Async)。而且您的代码仅在返回的Task完成后才处理Foo,这是我不喜欢的,因为它不必要地引入了并发性。 (2认同)

Ste*_*ary 85

如果您不需要async(即,您可以Task直接退货),那么请不要使用async.

在某些情况下return await,如果您要执行两个异步操作,则有用:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);
Run Code Online (Sandbox Code Playgroud)

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

更新:我写了一篇更详细的博客文章.

  • @MattSmith那不会编译.如果你想在第一行使用`await`,你也必须在第二行使用它. (23认同)
  • 你能否解释为什么`await`在第二种情况下有用?为什么不`返回SecondAwait(intermediate);`? (13认同)
  • @TomLint [这真的没有编译](http://tryroslyn.azurewebsites.net/#K4Zwlgdg5gBAygTxAFwKYFsDcAoUlaIoYB0AKgBYBOqAhgCb5k0gDWIO2NARipTQMbIY/ADbMQMAMIwA3thgKYAB2BcRYfjG68BQ0sxYAeSMgB8MAGJhKKAIIgEEfgAoAlDkXLV6zduR9BGH1WQ158czhUfgB7CDp7RxcTGEgVZHd5RUyFFTUNLQcnIINQ/3CYAFkaSASnN2zZBs8ANxpKFIg0SnRUBho0GABeLQB3aqErG2RalwyASDnqZGBKCHgo2PjCpM7Ubt6wftQMzwBfbFOgA=)假设的`返回类型SecondAwait`是`串,所述错误消息是:"CS4016:由于这是一个异步方法,因此返回表达式必须是'string'类型,而不是'Task <string>'". (3认同)
  • @svick你是对的。一旦将方法声明为“异步”,您就失去了直接返回任务的能力。我对我的一些代码感到困惑,这些代码直接从 **另一个** 异步方法返回任务而不是等待它。 (3认同)
  • 我和Matt有同样的问题,不会'返回SecondAwait(中间);`在那种情况下实现目标?我认为`return await`在这里也是多余的...... (2认同)
  • @svick,因为它们只是按顺序运行,是否应该将它们更改为正常调用,例如`var middle = First(); return Second(intermediate)` 以避免并行引入的开销。在这种情况下,异步调用是不必要的,不是吗? (2认同)
  • @cateyes我不确定"并行引入的开销"是什么意思,但`async`版本将使用*less*resources(threads)而不是同步版本. (2认同)

Ser*_*rvy 23

你想要这样做的唯一原因是,如果await在早期的代码中有其他一些,或者你在返回它之前以某种方式操纵结果.可能发生的另一种方式是通过try/catch改变异常处理方式.如果你没有做任何事情,那么你是对的,没有理由增加制作方法的开销async.

  • @Noseratio试试这两个.第一个编译.第二个没有.错误消息将告诉您问题.你将不会返回正确的类型.在`async`方法中,如果不返回任务,则返回任务的结果,然后将其包装. (11认同)
  • @TX_如果你想删除`async`那么你将如何等待第一个任务?如果要使用*any*awaits,则需要将方法标记为"async".如果该方法被标记为"async"并且您在代码中有一个`await`,那么您需要`等待'第二个异步操作,使其具有正确的类型.如果你刚删除`await`那么它就不会编译,因为返回值不是正确的类型.由于该方法是"async",因此结果始终包含在任务中. (10认同)
  • 和Stephen的回答一样,我不明白为什么`return await`是必要的(而不仅仅是返回子调用的任务)**即使在早期代码中有其他等待**.你能解释一下吗? (4认同)
  • @Servy,当然 - 你是对的.在后一种情况下,我们将显式返回`Task <Type>`,而`async`指示返回`Type`(编译器本身会变成`Task <Type>`). (3认同)
  • @Itsik当然,"async"只是用于明确连接延续的语法糖.你不需要*`async`来做任何事情,但是当做任何非平凡的异步操作时,它**显着*更容易使用.例如,您提供的代码实际上并不像您希望的那样传播错误,并且在更复杂的情况下正确地执行此操作开始变得非常困难.虽然你从不*需要*`async`,但我描述的情况是它增加使用价值的地方. (3认同)

And*_*ott 16

您可能需要等待结果的另一个案例是:

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

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,GetIFooAsync()必须等待结果,GetFooAsync因为T两种方法之间的类型不同,并且Task<Foo>不能直接赋值Task<IFoo>.但是,如果你等待的结果,它只是成为Foo直接分配给IFoo.然后异步方法只是重新打包内部Task<IFoo>和远离你的结果.


And*_*ott 5

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

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


hel*_*ker 5

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

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

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

从您的代码:

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

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

  • @Shoe 我不确定我是否理解“async/await”这件事。据我了解,`Task task = DoSomethingAsync()`,而`Something something = await DoSomethingAsync()` 都可以工作。第一个给你正确的任务,而第二个,由于`await`关键字,在任务完成后给你_result_。例如,我可以让`Task task = DoSomethingAsync(); 有些东西 = 等待任务;`。 (5认同)

hai*_*imb 5

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

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

例如:

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

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

  • 这是一篇关于此的好文章:https://vkontech.com/exploring-the-async-await-state-machine-stack-traces-and-refactoring-pitfalls/ (2认同)