返回void和返回Task有什么区别?(涉及IT技术)

146

在查看各种 C# Async CTP 示例时,我发现一些异步函数返回 void,而另一些则返回非泛型的 Task。 我可以理解返回 Task<MyType> 在异步操作完成后向调用方返回数据的有用性,但是我发现那些返回类型为 Task 的函数从来没有返回过任何数据。 为什么不返回 void

4个回答

229
SLaks和Killercam的回答很好,我想补充一些背景信息。您的第一个问题本质上是关于哪些方法可以标记为“async”的问题。
一个标记为“async”的方法可以返回void、Task或Task。它们之间有什么区别呢?
一个返回Task的异步方法可以被等待,当任务完成时,它会提供一个T。
一个返回Task的异步方法可以被等待,当任务完成时,任务的继续执行被调度。
一个返回void的异步方法无法被等待;它是一个“fire and forget”方法。它异步工作,但您无法知道它何时完成。这更加奇怪;正如SLaks所说,通常只有在创建异步事件处理程序时才会这样做。事件触发,处理程序执行;没有人会“等待”事件处理程序返回的任务,因为事件处理程序不会返回任务,即使它们返回了任务,什么代码会使用该任务呢?通常不是用户代码首先将控制权传递给处理程序。
您的第二个问题,在评论中,本质上是关于哪些方法可以await的问题:
哪些方法可以await?一个返回void的方法可以await吗?
不,一个返回void的方法无法await。编译器将await M()转换为对M().GetAwaiter()的调用,其中GetAwaiter可能是实例方法或扩展方法。被等待的值必须是可以获取awaiter的值;显然,返回void的方法不会产生可以获取awaiter的值。
返回Task的方法可以产生可等待的值。我们预计第三方将希望创建自己的类似于Task的对象实现,这些对象可以被等待,您将能够等待它们。但是,您将不允许声明返回除void、Task或Task之外的任何内容的async方法。
(更新:我的最后一句话可能会被C#的未来版本证明为虚假的;有一个提案允许异步方法返回任务类型以外的返回类型。)
(更新:上述功能已经添加到C# 7中。)

7
我认为唯一缺失的是空返回异步方法中异常处理的区别。 - João Angelo
10
假设一些异步工作抛出了异常。谁来捕获它? 启动异步任务的代码已经不在堆栈中了,甚至可能不在同一个线程上,而异常假定所有catch/finally块都在堆栈上。那么该怎么办呢?我们将异常信息存储在任务中,以便稍后检查。但如果该方法是没有返回值的,则用户代码无法使用任务。如何处理这种情况一直存在争议,我现在不记得我们决定采取什么措施了。 - Eric Lippert
8
我曾在BUILD活动上向Stephen Toub提出过这个问题。在.NET 4.0中,如果Task中存在未被处理的异常而又没有被观察到,那么一旦TPL检测到它们,进程最终会崩溃。在4.5中,他们改变了默认行为,使得未被观察到的异常仍通过TaskScheduler :: UnobservedTaskException事件报告,但不再使进程崩溃。如果你希望恢复旧的4.0行为,可以使用<runtime><ThrowUnobservedTaskExceptions enabled="true"/></runtime>选择重新启用。最有可能是为了支持无返回类型的异步方法而做出了这个更改。 - Drew Marsh
5
async void” 方法会在其开始执行时所处的同步上下文(SynchronizationContext)上引发异常,类似于同步事件处理程序的行为。@DrewMarsh:UnobservedTaskException 和运行时设置仅适用于“fire and forget”的异步Task方法,而不适用于“async void”方法。 - Stephen Cleary
1
异步异常处理信息的引用链接:http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx#11 - Luke Puplett
显示剩余2条评论

26

如果调用者想要等待任务或添加后续操作。

事实上,只有在编写事件处理程序时才无法返回Task的情况下,返回void才是唯一的原因。


我认为等待返回void类型的方法也是可能的 - 你能详细说明一下吗? - James Cadd
2
不行。如果方法返回void,你无法获取它生成的任务。(实际上,我不确定它是否真的生成了一个Task) - SLaks

24

返回TaskTask<T>的方法是可组合的——这意味着您可以在async方法中await它们。

async返回void的方法是不可组合的,但它们具有两个其他重要属性:

  1. 它们可以用作事件处理程序。
  2. 它们表示“顶层”异步操作。

当您处理一个维护未完成异步操作计数的上下文时,第二点非常重要。

ASP.NET上下文就是这样的上下文;如果您在asyncvoid方法中没有等待来自asyncTask方法的结果,则ASP.NET请求将太早完成。

另一个上下文是我为单元测试编写的AsyncContext(可在这里获取) - AsyncContext.Run方法跟踪未完成操作计数,并在其为零时返回。


12
类型Task<T>是任务并行库(TPL)的工作类型,它代表“将来会产生类型为T结果的一些工作/任务”的概念。表示“将在未来完成但不返回结果的工作”由非泛型任务类型表示。
精确地说,类型为T的结果将如何生成是特定任务的实现细节;工作可能被分配到本地机器上的另一个进程,到另一个线程等。TPL任务通常从当前进程的线程池中分配给工作线程,但这种实现细节并不是Task<T>类型的基本内容;相反,Task<T>可以表示任何产生T的高延迟操作。
根据您上面的评论: await表达式的意思是“评估此表达式以获取表示将来产生结果的工作对象。注册当前方法的剩余部分作为与该任务继续关联的回调。一旦生成该任务并注册回调,立即将控制权返回给我的调用者”。这与常规方法调用相反/对比,后者意味着“记住你正在做什么,运行此方法直到完全完成,然后从你离开的地方继续,并知道方法的结果”。
编辑:我应该引用Eric Lippert在2011年10月MSDN杂志中的文章,因为这对我理解这些东西非常有帮助。
要获取更多信息和白皮书,请参见此处
希望这对您有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接