为什么我们需要async关键字?


19

我刚开始在.Net 4.5中开始使用异步/等待。我最初好奇的一件事是,为什么async关键字是必需的?我读到的解释是,它是一个标记,因此编译器知道一个方法在等待。但是似乎编译器应该能够在没有关键字的情况下解决问题。那还有什么作用呢?


2
这是一篇非常不错的博客文章(系列),其中详细介绍了C#中的关键字以及F#中的相关功能。
保罗

我认为这主要是开发人员的标记,而不是编译器的标记。
CodesInChaos


@svick谢谢,这就是我想要的。现在变得非常合理。
ConditionRacer

Answers:


23

这里有几个答案,并且所有人都在谈论异步方法的作用,但是没有一个人回答这个问题,这就是为什么async需要将关键字用作函数声明中的关键字。

这不是“指导编译器以特殊方式转换函数”;await一个人就能做到。为什么?由于C#已经具有另一种机制,其中方法主体中存在特殊关键字会导致编译器在方法主体上执行极端(非常类似于async/await)转换yield

除了yield在C#中不是它自己的关键字,理解原因也将解释async。与大多数支持这种机制的语言不同,在C#中,您不必说yield value;必须说yield return value;。为什么?因为它是在C#已经存在之后添加到语言中的,并且假设某个地方的某个人可能已经将其yield用作变量名是非常合理的。但是,由于没有在<variable name> return语法上正确的先前存在的场景,因此yield return对该语言进行了添加,以使其能够引入生成器,同时保持与现有代码的100%向后兼容性。

这就是为什么将async其添加为函数修饰符的原因:避免破坏用作await变量名的现有代码。由于尚无任何async方法,因此旧代码不会无效,在新代码中,编译器可以使用async标记的存在来知道await应将其视为关键字而不是标识符。


3
这也差不多是本文所说的(最初由@svick在评论中发布),但是没有人将其发布为答案。只用了两年半!谢谢:)
ConditionRacer

这是否意味着在将来的某些版本中,async可以放弃指定关键字的要求?显然,这将是一次BC中断,但可以认为它影响很少的项目,并且是升级的直接要求(即更改变量名)。
Quolonel问题

6

它将方法从普通方法更改为带有回调的对象,这需要完全不同的代码生成方法

并且当发生如此剧烈的事情时,习惯将其清楚地表示出来(我们从C ++中学到了这一课)


因此,对于阅读器/程序员而言,这确实是什么,而对于编译器而言,则不是很多?
ConditionRacer

@ Justin984它绝对有助于向编译器发送信号,表示需要进行特殊处理
棘手怪胎

我想我很好奇编译器为什么需要它。难道它只是解析方法并看到await在方法主体中的某个位置吗?
ConditionRacer

当您要同时调度多个asyncs时它们会有所帮助,这样它们就不会一个接一个地运行,而是同时运行(如果至少有线程可用)
棘手怪胎

@ Justin984:并非每个返回的方法Task<T>实际上都具有方法的特性async-例如,您可能只运行了一些业务/工厂逻辑,然后委托给其他一些返回的方法Task<T>async方法Task<T>签名中的返回类型为,但实际上并不返回Task<T>,而是返回T。为了在不使用async关键字的情况下弄清所有这些情况,C#编译器将不得不对该方法进行各种深度检查,这可能会使其速度大大降低,并导致各种歧义。
Aaronaught

4

使用诸如“异步”或“不安全”之类的关键字的整个想法是消除关于如何对待它们修改的代码的歧义。对于async关键字,它告诉编译器将修改后的方法视为不需要立即返回的东西。这允许使用该方法的线程继续执行而不必等待该方法的结果。这实际上是代码优化。


我倾向于投票,因为尽管第一段是正确的,但与中断的比较是不正确的(据我所知)。
保罗

我认为它们在目的上有些相似,但是为了清楚起见,我将其删除。
世界工程师

在我看来,中断更像是事件,而不是异步代码-中断会导致上下文切换并在恢复正常代码之前运行完成(并且正常代码也可能完全不了解它的运行),而对于异步代码,该方法可能会恢复调用线程之前尚未完成。我看到了您想说的是什么机制,这些机制要求在后台进行不同的实现,但是中断并不能帮助我说明这一点。(其余为+1,顺便说一句)
保罗

0

好,这是我的看法。

有几十年来被称为协程的东西。(“ Knuth and Hopper”类“数十年”)它们是子例程的概括,例如,它们不仅在函数的start和return语句中获取和释放控制,而且还在特定点(暂停点)进行控制。子例程是没有暂停点的协程。

使用C宏可以轻松实现它们,如以下有关“ protothreads”的论文所示。(http://dunkels.com/adam/dunkels06protothreads.pdf)阅读。我会等...

这样做的底线是宏会在每个挂起点创建一个big switch以及一个case标签。在每个挂起点,该函数都存储紧随其后的case标签的值,以便它知道下次调用时在哪里恢复执行。然后它将控制权返回给调用者。

无需修改“原型线程”中描述的代码的明显控制流即可完成此操作。

现在想象一下,您有一个大循环依次调用所有这些“ protothreads”,并且在单个线程上同时执行“ protothreads”。

这种方法有两个缺点:

  1. 您不能在恢复之间将状态保持在局部变量中。
  2. 您不能从任意调用深度挂起“ protothread”。(所有暂停点必须位于0级)

两种方法都有解决方法:

  1. 必须将所有局部变量上拉到protothread的上下文(protothread必须存储其下一个恢复点的事实已经需要的上下文)
  2. 如果您确实需要从一个protothread调用另一个protothread,请“产生”一个子protothread并暂停直到该子protothread完成。

而且,如果您有编译器支持来执行宏和变通方法所做的重写工作,那么,您可以按照您的意图编写protothread代码,并使用关键字插入悬挂点。

而这也正是asyncawait都是关于:创建(无堆栈)协同程序。

C#中的协程被定义为(通用或非通用)类的对象Task

我发现这些关键字极具误导性。我的思想阅读是:

  • async 作为“可疑”
  • await 作为“暂停直到完成”
  • Task 作为“未来……”

现在。我们真的需要标记功能async吗?除了说应该触发代码重写机制以使函数成为协程外,它还解决了一些歧义。考虑下面的代码。

public Task<object> AmIACoroutine() {
    var tcs = new TaskCompletionSource<object>();
    return tcs.Task;
}

假设这async不是强制性的,这是协程还是正常功能?编译器是否应该将其重写为协程?两种可能都有不同的最终语义。

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.