当您可以直接返回 Task<T> 时,为什么要使用 async 和 return await?

Why use async and return await, when you can return Task<T> directly?

提问人:TX_ 提问时间:9/30/2013 最后编辑:Theodor ZouliasTX_ 更新时间:10/19/2023 访问量:70563

问:

有没有这样的写法:

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<T> 时,为什么要使用 return await 构造?

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

C# .NET 异步 async-await 任务

评论

7赞 Fabio Marcolini 9/30/2013
我认为你看到这一点的唯一原因是因为人们通过模仿来学习,并且通常(如果他们不需要)他们使用他们能找到的最简单的解决方案。所以人们看到那个代码,使用那个代码,他们看到它有效,从现在开始,对他们来说,这是正确的方法......在这种情况下等待是没有用的
22赞 noseratio 1/13/2014
至少有一个重要的区别:异常传播
1赞 Slava Lishnevsky 3/29/2018
我也不明白,根本无法理解整个概念,没有任何意义。据我所知,如果一个方法有返回类型,它必须有一个返回关键字,这不是C#语言的规则吗?
1赞 David Klempfner 12/11/2019
不过,OP 的问题@monstro确实有返回声明?
1赞 Oliver 3/11/2022
github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/......

答:

142赞 Stephen Cleary 9/30/2013 #1

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

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

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

有关性能的详细信息,请参阅 Stephen Toub 的 MSDN 文章和有关该主题的视频async

更新:我写了一篇博文,详细介绍了这一点。

评论

20赞 Matt Smith 10/1/2013
您能否解释一下为什么在第二种情况下有用?为什么不做呢?awaitreturn SecondAwait(intermediate);
2赞 TX_ 10/1/2013
我和马特有同样的问题,在这种情况下,不会也实现目标?我认为这里也是多余的......return SecondAwait(intermediate);return await
33赞 svick 10/1/2013
@MattSmith 那不会编译。如果你想在第一行使用它,你也必须在第二行使用它。await
10赞 svick 11/23/2016
@TomLint 它真的无法编译。假设返回类型为 'string',则错误消息为:“CS4016:由于这是一个异步方法,因此返回表达式的类型必须为 'string' 而不是 'Task<string>'”。SecondAwait
3赞 Tom Lint 12/7/2016
@svick 你是对的。将方法声明为“异步”后,您将无法直接返回 Task。我对我的一些代码感到困惑,这些代码直接从另一个异步方法返回任务而不是等待它。
30赞 Servy 9/30/2013 #2

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

评论

4赞 TX_ 10/1/2013
与 Stephen 的回答一样,我不明白为什么有必要(而不仅仅是返回子调用任务),即使前面的代码中还有其他一些等待。您能解释一下吗?return await
12赞 Servy 10/1/2013
@TX_ 如果您想删除,那么您将如何等待第一个任务?您需要将该方法标记为要使用任何 awaits。如果该方法被标记为 并且您有较早的代码,则需要执行第二个异步操作才能使其类型正确。如果您刚刚删除,则它不会编译,因为返回值的类型不正确。由于方法是,因此结果始终包装在任务中。asyncasyncasyncawaitawaitawaitasync
13赞 Servy 10/1/2013
@Noseratio 试试这两个。第一个编译。第二个没有。错误消息将告诉您问题所在。您不会返回正确的类型。在方法中,如果不返回任务,则返回任务的结果,然后将包装该结果。async
3赞 noseratio 10/1/2013
@Servy,当然——你是对的。在后一种情况下,我们将显式返回,而指令返回(编译器本身会变成)。Task<Type>asyncTypeTask<Type>
3赞 Servy 2/26/2016
@Itsik 当然,这只是用于显式连接延续的句法糖。您不需要做任何事情,但是在执行任何重要的异步操作时,使用起来会变得非常容易。例如,您提供的代码实际上并没有像您希望的那样传播错误,并且在更复杂的情况下正确地传播错误开始变得更加困难。虽然你永远不需要,但我描述的情况是使用它增加价值的地方。asyncasyncasync
269赞 svick 10/1/2013 #3

有一种偷偷摸摸的情况,即在正常方法和方法中的行为不同:当与块组合时(或者更一般地说,块中的任何)。returnreturn awaitasyncusingreturn 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();
    }
}

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

评论

5赞 ghord 9/23/2014
为了完整起见,首先您应该返回foo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
11赞 svick 9/23/2014
@ghord那行不通,返回.你需要类似的东西。但是我不知道当您可以使用第二个选项时,为什么要这样做。Dispose()voidreturn foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });
1赞 ghord 9/23/2014
@svick 你是对的,它应该更符合 .用例非常简单:如果您使用的是 .NET 4.0(像大多数一样),您仍然可以以这种方式编写异步代码,这些代码可以在 4.5 应用程序中很好地调用。{ var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }
2赞 svick 9/23/2014
@ghord 如果您使用的是 .Net 4.0 并且想要编写异步代码,则可能应该使用 Microsoft.Bcl.Async。而且你的代码只有在返回完成后才会处理,我不喜欢这一点,因为它不必要地引入了并发性。FooTask
1赞 ghord 9/23/2014
@svick 您的代码也会等到任务完成。此外,由于对 KB2468871 的依赖以及将 .NET 4.0 异步代码库与适当的 4.5 异步代码一起使用时发生冲突,Microsoft.Bcl.Async 对我来说不可用。
22赞 Andrew Arnott 5/8/2015 #4

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

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

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

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

评论

1赞 StuartLC 4/12/2019
同意,这真的很烦人——我相信根本原因是不变的。Task<>
12赞 Andrew Arnott 5/8/2015 #5

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

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

5赞 heltonbiker 1/6/2017 #6

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

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

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

从您的代码:

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

不熟悉语法的人(例如我)可能会认为此方法应该返回 ,但由于它标有 ,这意味着它的实际返回类型是 。 如果你只使用 ,你会返回一个 Task,它不会编译。正确的方法是返回任务的结果,因此.Task<SomeResult>asyncSomeResultreturn foo.DoAnotherThingAsync()return await

评论

1赞 jamesSampica 1/6/2017
“实际返回类型”。啊?async/await 不会更改返回类型。在你的示例中会给你一个任务,而不是var task = DoSomethingAsync();T
5赞 heltonbiker 1/6/2017
@Shoe我不确定我是否理解这件事。据我了解,虽然两者都有效。第一个给你正确的任务,而第二个,由于关键词,在任务完成后给你一个结果。例如,我可以有.async/awaitTask task = DoSomethingAsync()Something something = await DoSomethingAsync()awaitTask task = DoSomethingAsync(); Something something = await task;
22赞 haimb 1/16/2019 #7

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

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

例如:

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

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

评论

2赞 Jonatan Dragon 7/1/2021
这是一篇关于此的好文章:vkontech.com/......
3赞 Sedat Kapanoglu 4/9/2022 #8

您可能想要的另一个原因:语法可以避免在 和 类型之间遇到不匹配。例如,即使 SubTask 方法返回但其调用方返回 ,下面的代码仍然有效。return awaitawaitTask<T>ValueTask<T>Task<T>ValueTask<T>

async Task<T> SubTask()
{
...
}

async ValueTask<T> DoSomething()
{
  await UnimportantTask();
  return await SubTask();
}

如果在行上跳过 await,则会收到编译器错误 CS0029:DoSomething()

无法将类型“System.Threading.Tasks.Task<BlaBla>”隐式转换为“System.Threading.Tasks.ValueTask<BlaBla>”。

如果您尝试显式类型转换 CS0030,您也会得到它。

顺便说一下,这是 .NET Framework。我完全可以预见到一条评论说“这在 .NET hypothetical_version 中已修复”,我还没有测试过它。:)

评论

0赞 KwahuNashoba 8/17/2022
我想说,称其为皈依有点误导,因为据我所知,它与皈依无关。您在上面所做的是从 SubTask 获取 T 结果,然后返回该结果,而不是任务。所以 T 是同一个 T,没有任何转换。
2赞 Sedat Kapanoglu 8/18/2022
@KwahuNashoba 现在编辑它。它看起来如何?
1赞 KwahuNashoba 8/19/2022
伟大!感谢您积极主动:)
1赞 Luke Vo 1/30/2023 #9

非 await 方法的另一个问题是有时您无法隐式转换返回类型,尤其是:Task<IEnumerable<T>>

async Task<List<string>> GetListAsync(string foo) => new();

// This method works
async Task<IEnumerable<string>> GetMyList() => await GetListAsync("myFoo");

// This won't work
Task<IEnumerable<string>> GetMyListNoAsync() => GetListAsync("myFoo");

错误:

无法将类型“System.Threading.Tasks.Task<System.Collections.Generic.List>”隐式转换为“System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable>”

1赞 Oliver 10/19/2023 #10

这将阻止创建将在编译时在后台创建的任务状态机。但根据 David 的说法,出于以下原因,您应该更喜欢 async/await 而不是直接返回 Task

  • 异步和同步异常被规范化为始终是异步的。
  • 代码更易于修改(例如,考虑添加 using)。
  • 异步方法的诊断更容易(调试挂起等)。
  • 抛出的异常将自动包装在返回的 Task 中,而不是用实际的异常让调用者感到惊讶。
  • 异步本地变量不会从异步方法中泄漏出来。如果在非异步方法中设置了 async local,它将从该调用中“泄漏”出来。

💡注意:使用异步状态时存在性能注意事项 机器直接返回任务。它总是更快 直接返回任务,因为它做的工作较少,但你最终会 改变行为并可能失去一些好处 异步状态机。

如果你真的想打破异步,你应该知道异步是病毒式的

一旦你异步,你的所有调用方都应该是异步的,因为努力 除非整个调用堆栈是异步的,否则异步就等于什么都没有。在 在许多情况下,部分异步可能比完全异步更糟糕 同步。因此,最好全力以赴,把一切都做好 异步。